init
This commit is contained in:
@@ -0,0 +1,735 @@
|
||||
local msg = require('mp.msg')
|
||||
local utils = require("mp.utils")
|
||||
|
||||
local function extract_url(url)
|
||||
local path = url:match("^https?://[^/]+(/[^%?]*)")
|
||||
return path
|
||||
end
|
||||
|
||||
local function generateXSignature(url, time, appid, app_accept)
|
||||
local url_path = extract_url(url)
|
||||
if not url_path then
|
||||
return nil
|
||||
end
|
||||
|
||||
local dataToHash = string.format("%s%d%s%s", AES.ECB.decrypt(KEY, Base64.decode(appid)),
|
||||
time, url_path, AES.ECB.decrypt(KEY, Base64.decode(app_accept)))
|
||||
local hash = Sha256(dataToHash)
|
||||
local base64Hash = Base64.encode(hex_to_bin(hash))
|
||||
return base64Hash
|
||||
end
|
||||
|
||||
-- 写入history.json
|
||||
-- 读取episodeId获取danmaku
|
||||
function set_episode_id(input, from_menu)
|
||||
from_menu = from_menu or false
|
||||
DANMAKU.source = "dandanplay"
|
||||
for url, source in pairs(DANMAKU.sources) do
|
||||
if source.from == "api_server" then
|
||||
if source.fname and file_exists(source.fname) then
|
||||
os.remove(source.fname)
|
||||
end
|
||||
|
||||
if not source.from_history then
|
||||
DANMAKU.sources[url] = nil
|
||||
else
|
||||
DANMAKU.sources[url]["fname"] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
local episodeId = tonumber(input)
|
||||
write_history(episodeId)
|
||||
set_danmaku_button()
|
||||
if options.load_more_danmaku then
|
||||
fetch_danmaku_all(episodeId, from_menu)
|
||||
else
|
||||
fetch_danmaku(episodeId, from_menu)
|
||||
end
|
||||
end
|
||||
|
||||
-- 回退使用额外的弹幕获取方式
|
||||
function get_danmaku_fallback(query)
|
||||
local url = options.fallback_server .. "/?url=" .. query
|
||||
msg.verbose("尝试获取弹幕:" .. url)
|
||||
local temp_file = "danmaku-" .. PID .. DANMAKU.count .. ".xml"
|
||||
local danmaku_xml = utils.join_path(DANMAKU_PATH, temp_file)
|
||||
DANMAKU.count = DANMAKU.count + 1
|
||||
local arg = {
|
||||
"curl",
|
||||
"-L",
|
||||
"-s",
|
||||
"--compressed",
|
||||
"--user-agent",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0",
|
||||
"--output",
|
||||
danmaku_xml,
|
||||
url,
|
||||
}
|
||||
|
||||
call_cmd_async(arg, function(error)
|
||||
async_running = false
|
||||
if error then
|
||||
show_message("HTTP 请求失败,打开控制台查看详情", 5)
|
||||
msg.error(error)
|
||||
return
|
||||
end
|
||||
if file_exists(danmaku_xml) then
|
||||
if query:find("iqiyi%.com") ~= nil then
|
||||
DANMAKU.strict = true
|
||||
end
|
||||
save_danmaku_downloaded(query, danmaku_xml)
|
||||
load_danmaku(true)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- 返回弹幕请求参数
|
||||
function make_danmaku_request_args(method, url, headers, body)
|
||||
local args = {
|
||||
"curl",
|
||||
"-L",
|
||||
"-X",
|
||||
method,
|
||||
"-H",
|
||||
"Accept: application/json",
|
||||
"-H",
|
||||
"User-Agent: " .. options.user_agent,
|
||||
}
|
||||
|
||||
if headers then
|
||||
for k, v in pairs(headers) do
|
||||
table.insert(args, '-H')
|
||||
table.insert(args, string.format('%s: %s', k, v))
|
||||
end
|
||||
end
|
||||
|
||||
if body then
|
||||
table.insert(args, '-d')
|
||||
table.insert(args, utils.format_json(body))
|
||||
table.insert(args, '-H')
|
||||
table.insert(args, 'Content-Type: application/json')
|
||||
end
|
||||
|
||||
if url:find("api%.dandanplay%.") then
|
||||
local time = os.time()
|
||||
local appid = "UgjRIH45lE1BBLNmir1WKw=="
|
||||
local app_accept = "SzuWlFZAPRMqeWf9qmfp8dcvYr3hvxuSrIRZuAeEfko="
|
||||
table.insert(args, '-H')
|
||||
table.insert(args, string.format('X-AppId: %s', AES.ECB.decrypt(KEY, Base64.decode(appid))))
|
||||
table.insert(args, '-H')
|
||||
table.insert(args, string.format('X-Signature: %s', generateXSignature(url, time, appid, app_accept)))
|
||||
table.insert(args, '-H')
|
||||
table.insert(args, string.format('X-Timestamp: %s', time))
|
||||
end
|
||||
|
||||
table.insert(args, url)
|
||||
|
||||
return args
|
||||
end
|
||||
|
||||
-- 尝试通过解析文件名匹配剧集
|
||||
local function match_episode(animeTitle, bangumiId, episode_num)
|
||||
local url = options.api_server .. "/api/v2/bangumi/" .. bangumiId
|
||||
local args = make_danmaku_request_args("GET", url)
|
||||
|
||||
if args == nil then
|
||||
return
|
||||
end
|
||||
|
||||
call_cmd_async(args, function(error, json)
|
||||
async_running = false
|
||||
if error then
|
||||
show_message("HTTP 请求失败,打开控制台查看详情", 5)
|
||||
msg.error(error)
|
||||
return
|
||||
end
|
||||
|
||||
local data = utils.parse_json(json)
|
||||
if not data or not data.bangumi or not data.bangumi.episodes then
|
||||
msg.info("无结果")
|
||||
return
|
||||
end
|
||||
|
||||
for _, episode in ipairs(data.bangumi.episodes) do
|
||||
local ep_num = tonumber(episode.episodeNumber)
|
||||
if ep_num and ep_num == tonumber(episode_num) then
|
||||
DANMAKU.anime = animeTitle
|
||||
DANMAKU.episode = episode.episodeTitle
|
||||
set_episode_id(episode.episodeId)
|
||||
break
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function match_anime()
|
||||
local animes = {}
|
||||
local anime_type = "tvseries"
|
||||
local type_count = 0
|
||||
local title, season_num, episode_num = parse_title()
|
||||
if not episode_num then
|
||||
msg.info("无法解析剧集信息")
|
||||
return
|
||||
end
|
||||
|
||||
if title:match("OVA") or title:match("OAD") then
|
||||
anime_type = "ova"
|
||||
end
|
||||
|
||||
local encoded_query = url_encode(title)
|
||||
local url = options.api_server .. "/api/v2/search/anime"
|
||||
local params = "keyword=" .. encoded_query
|
||||
local full_url = url .. "?" .. params
|
||||
local args = make_danmaku_request_args("GET", full_url)
|
||||
|
||||
if not args then return end
|
||||
|
||||
call_cmd_async(args, function(error, json)
|
||||
async_running = false
|
||||
if error then
|
||||
show_message("HTTP 请求失败,打开控制台查看详情", 5)
|
||||
msg.error(error)
|
||||
return
|
||||
end
|
||||
|
||||
local data = utils.parse_json(json)
|
||||
if not data or not data.animes then
|
||||
msg.info("无结果")
|
||||
return
|
||||
end
|
||||
|
||||
for _, anime in ipairs(data.animes) do
|
||||
if anime.type == anime_type then
|
||||
type_count = type_count + 1
|
||||
table.insert(animes, anime)
|
||||
end
|
||||
end
|
||||
|
||||
if type_count == 1 then
|
||||
match_episode(animes[1].animeTitle, animes[1].bangumiId, episode_num)
|
||||
elseif type_count > 1 and season_num then
|
||||
local best_match, best_score = nil, -1
|
||||
local target_title = title
|
||||
if tonumber(season_num) > 1 then
|
||||
target_title = title .. " 第" .. number_to_chinese(season_num) .. "季"
|
||||
end
|
||||
for _, anime in ipairs(animes) do
|
||||
if anime.animeTitle:match("第一[季部]") and tonumber(season_num) == 1 then
|
||||
target_title = title .. " 第一季"
|
||||
end
|
||||
local score = jaro_winkler(target_title, anime.animeTitle)
|
||||
msg.debug(("候选: %s -> 相似度 %.3f"):format(anime.animeTitle, score))
|
||||
if score > best_score then
|
||||
best_score = score
|
||||
best_match = anime
|
||||
end
|
||||
end
|
||||
|
||||
if best_match and best_score >= 0.75 then
|
||||
msg.info(("模糊匹配选中: %s (score=%.2f)"):format(best_match.animeTitle, best_score))
|
||||
match_episode(best_match.animeTitle, best_match.bangumiId, episode_num)
|
||||
else
|
||||
msg.info("匹配到多个结果,但相似度不足,请手动搜索")
|
||||
end
|
||||
else
|
||||
msg.info("没有找到合适的匹配结果")
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- 执行哈希匹配获取弹幕
|
||||
local function match_file(file_path, file_name, callback)
|
||||
-- 计算文件哈希
|
||||
local hash = nil
|
||||
local file_info = utils.file_info(file_path)
|
||||
if file_info and file_info.size > 16 * 1024 * 1024 then
|
||||
local file, error = io.open(normalize(file_path), 'rb')
|
||||
if file and not error then
|
||||
local m = MD5.new()
|
||||
for _ = 1, 16 * 1024 do
|
||||
local content = file:read(1024)
|
||||
if not content then
|
||||
break
|
||||
end
|
||||
m:update(content)
|
||||
end
|
||||
file:close()
|
||||
hash = m:finish()
|
||||
end
|
||||
end
|
||||
|
||||
if hash then msg.info('hash:', hash) end
|
||||
|
||||
local title, season_num, episode_num = parse_title()
|
||||
if title and episode_num then
|
||||
if season_num then
|
||||
file_name = title .. " S" .. season_num .. "E" .. episode_num
|
||||
else
|
||||
file_name = title .. " E" .. episode_num
|
||||
end
|
||||
else
|
||||
file_name = title
|
||||
end
|
||||
|
||||
local url = options.api_server .. "/api/v2/match"
|
||||
local args = make_danmaku_request_args("POST", url, {
|
||||
["Content-Type"] = "application/json"
|
||||
}, {
|
||||
fileName = file_name,
|
||||
fileHash = hash or "",
|
||||
matchMode = "hashAndFileName"
|
||||
}
|
||||
)
|
||||
|
||||
if not args then return end
|
||||
|
||||
call_cmd_async(args, function(error, json)
|
||||
async_running = false
|
||||
if error then
|
||||
show_message("HTTP 请求失败,打开控制台查看详情", 5)
|
||||
callback(error)
|
||||
return
|
||||
end
|
||||
local data = utils.parse_json(json)
|
||||
if not data or not data.isMatched or #data.matches > 1 then
|
||||
callback("没有匹配的剧集")
|
||||
return
|
||||
end
|
||||
|
||||
DANMAKU.anime = data.matches[1].animeTitle
|
||||
DANMAKU.episode = data.matches[1].episodeTitle
|
||||
|
||||
-- 获取并加载弹幕数据
|
||||
set_episode_id(data.matches[1].episodeId)
|
||||
end)
|
||||
end
|
||||
|
||||
-- 异步获取弹幕数据
|
||||
function fetch_danmaku_data(args, callback)
|
||||
call_cmd_async(args, function(error, json)
|
||||
async_running = false
|
||||
if error then
|
||||
show_message("获取数据失败", 3)
|
||||
msg.error("HTTP 请求失败:" .. error)
|
||||
return
|
||||
end
|
||||
local data = utils.parse_json(json)
|
||||
callback(data)
|
||||
end)
|
||||
end
|
||||
|
||||
-- 保存弹幕数据
|
||||
function save_danmaku_data(comments, query, danmaku_source)
|
||||
local temp_file = "danmaku-" .. PID .. DANMAKU.count .. ".json"
|
||||
local danmaku_file = utils.join_path(DANMAKU_PATH, temp_file)
|
||||
DANMAKU.count = DANMAKU.count + 1
|
||||
local success = save_danmaku_json(comments, danmaku_file)
|
||||
|
||||
if success then
|
||||
if DANMAKU.sources[query] ~= nil then
|
||||
if DANMAKU.sources[query].fname and file_exists(DANMAKU.sources[query].fname) then
|
||||
os.remove(DANMAKU.sources[query].fname)
|
||||
end
|
||||
DANMAKU.sources[query]["fname"] = danmaku_file
|
||||
else
|
||||
DANMAKU.sources[query] = {from = danmaku_source, fname = danmaku_file}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function save_danmaku_downloaded(url, downloaded_file)
|
||||
if DANMAKU.sources[url] ~= nil then
|
||||
if DANMAKU.sources[url].fname and file_exists(DANMAKU.sources[url].fname) then
|
||||
os.remove(DANMAKU.sources[url].fname)
|
||||
end
|
||||
DANMAKU.sources[url]["fname"] = downloaded_file
|
||||
else
|
||||
DANMAKU.sources[url] = {from = "user_custom", fname = downloaded_file}
|
||||
end
|
||||
end
|
||||
|
||||
-- 处理弹幕数据
|
||||
function handle_danmaku_data(query, data, from_menu)
|
||||
local comments = data["comments"]
|
||||
local count = data["count"]
|
||||
|
||||
-- 如果没有数据,进行重试
|
||||
if count == 0 then
|
||||
show_message("服务器无缓存数据,再次尝试请求", 30)
|
||||
msg.verbose("服务器无缓存数据,再次尝试请求")
|
||||
-- 等待 2 秒后重试
|
||||
local start = os.time()
|
||||
while os.time() - start < 2 do
|
||||
-- 空循环,等待 2 秒
|
||||
end
|
||||
-- 重新发起请求
|
||||
local url = options.api_server .. "/api/v2/extcomment?url=" .. url_encode(query)
|
||||
local args = make_danmaku_request_args("GET", url)
|
||||
|
||||
if args == nil then
|
||||
return
|
||||
end
|
||||
|
||||
fetch_danmaku_data(args, function(retry_data)
|
||||
if not retry_data or not retry_data["comments"] or retry_data["count"] == 0 then
|
||||
get_danmaku_fallback(query)
|
||||
return
|
||||
end
|
||||
save_danmaku_data(retry_data["comments"], query, "user_custom")
|
||||
load_danmaku(from_menu)
|
||||
end)
|
||||
else
|
||||
save_danmaku_data(comments, query, "user_custom")
|
||||
load_danmaku(from_menu)
|
||||
end
|
||||
end
|
||||
|
||||
-- 处理第三方弹幕数据
|
||||
function handle_related_danmaku(index, relateds, related, shift, callback)
|
||||
local url = options.api_server .. "/api/v2/extcomment?url=" .. url_encode(related["url"])
|
||||
show_message(string.format("正在从第三方库装填弹幕 [%d/%d]", index, #relateds), 30)
|
||||
msg.verbose("正在从第三方库装填弹幕:" .. url)
|
||||
|
||||
local args = make_danmaku_request_args("GET", url)
|
||||
|
||||
if args == nil then
|
||||
return
|
||||
end
|
||||
|
||||
fetch_danmaku_data(args, function(data)
|
||||
local comments = {}
|
||||
if data and data["comments"] then
|
||||
if data["count"] == 0 then
|
||||
-- 如果没有数据,稍等 2 秒重试
|
||||
local start = os.time()
|
||||
while os.time() - start < 2 do
|
||||
-- 空循环,等待 2 秒
|
||||
end
|
||||
fetch_danmaku_data(args, function(data)
|
||||
for _, comment in ipairs(data["comments"]) do
|
||||
comment["shift"] = shift
|
||||
table.insert(comments, comment)
|
||||
end
|
||||
callback(comments)
|
||||
end)
|
||||
else
|
||||
for _, comment in ipairs(data["comments"]) do
|
||||
comment["shift"] = shift
|
||||
table.insert(comments, comment)
|
||||
end
|
||||
callback(comments)
|
||||
end
|
||||
else
|
||||
show_message("无数据", 3)
|
||||
msg.info("无数据")
|
||||
callback(comments)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- 处理dandan库的弹幕数据
|
||||
function handle_main_danmaku(url, from_menu)
|
||||
show_message("正在从弹弹Play库装填弹幕", 30)
|
||||
msg.verbose("尝试获取弹幕:" .. url)
|
||||
local args = make_danmaku_request_args("GET", url)
|
||||
|
||||
if args == nil then
|
||||
return
|
||||
end
|
||||
|
||||
fetch_danmaku_data(args, function(data)
|
||||
if not data or not data["comments"] then
|
||||
show_message("无数据", 3)
|
||||
msg.info("无数据")
|
||||
return
|
||||
end
|
||||
|
||||
local comments = data["comments"]
|
||||
local count = data["count"]
|
||||
|
||||
if count == 0 then
|
||||
if DANMAKU.sources[url] == nil then
|
||||
DANMAKU.sources[url] = {from = "api_server"}
|
||||
end
|
||||
load_danmaku(from_menu)
|
||||
return
|
||||
end
|
||||
|
||||
save_danmaku_data(comments, url, "api_server")
|
||||
load_danmaku(from_menu)
|
||||
end)
|
||||
end
|
||||
|
||||
-- 处理获取到的数据
|
||||
function handle_fetched_danmaku(data, url, from_menu)
|
||||
if data and data["comments"] then
|
||||
if data["count"] == 0 then
|
||||
if DANMAKU.sources[url] == nil then
|
||||
DANMAKU.sources[url] = {from = "api_server"}
|
||||
end
|
||||
show_message("该集弹幕内容为空,结束加载", 3)
|
||||
msg.verbose("该集弹幕内容为空,结束加载")
|
||||
return
|
||||
end
|
||||
save_danmaku_data(data["comments"], url, "api_server")
|
||||
load_danmaku(from_menu)
|
||||
else
|
||||
show_message("无数据", 3)
|
||||
msg.info("无数据")
|
||||
end
|
||||
end
|
||||
|
||||
-- 匹配弹幕库 comment, 仅匹配dandan本身弹幕库
|
||||
-- 通过danmaku api(url)+id获取弹幕
|
||||
function fetch_danmaku(episodeId, from_menu)
|
||||
local url = options.api_server .. "/api/v2/comment/" .. episodeId .. "?withRelated=true&chConvert=0"
|
||||
show_message("弹幕加载中...", 30)
|
||||
msg.verbose("尝试获取弹幕:" .. url)
|
||||
local args = make_danmaku_request_args("GET", url)
|
||||
|
||||
if args == nil then
|
||||
return
|
||||
end
|
||||
|
||||
fetch_danmaku_data(args, function(data)
|
||||
handle_fetched_danmaku(data, url, from_menu)
|
||||
end)
|
||||
end
|
||||
|
||||
-- 主函数:获取所有相关弹幕
|
||||
function fetch_danmaku_all(episodeId, from_menu)
|
||||
local url = options.api_server .. "/api/v2/related/" .. episodeId
|
||||
show_message("弹幕加载中...", 30)
|
||||
msg.verbose("尝试获取弹幕:" .. url)
|
||||
local args = make_danmaku_request_args("GET", url)
|
||||
|
||||
if args == nil then
|
||||
return
|
||||
end
|
||||
|
||||
fetch_danmaku_data(args, function(data)
|
||||
if not data or not data["relateds"] then
|
||||
show_message("无数据", 3)
|
||||
msg.info("无数据")
|
||||
return
|
||||
end
|
||||
|
||||
-- 处理所有的相关弹幕
|
||||
local relateds = data["relateds"]
|
||||
local function process_related(index)
|
||||
if index > #relateds then
|
||||
-- 所有相关弹幕加载完成后,开始加载主库弹幕
|
||||
url = options.api_server .. "/api/v2/comment/" .. episodeId .. "?withRelated=false&chConvert=0"
|
||||
handle_main_danmaku(url, from_menu)
|
||||
return
|
||||
end
|
||||
|
||||
local related = relateds[index]
|
||||
local shift = related["shift"]
|
||||
|
||||
-- 处理当前的相关弹幕
|
||||
handle_related_danmaku(index, relateds, related, shift, function(comments)
|
||||
if #comments == 0 then
|
||||
if DANMAKU.sources[related["url"]] == nil then
|
||||
DANMAKU.sources[related["url"]] = {from = "api_server"}
|
||||
end
|
||||
else
|
||||
save_danmaku_data(comments, related["url"], "api_server")
|
||||
end
|
||||
|
||||
-- 继续处理下一个相关弹幕
|
||||
process_related(index + 1)
|
||||
end)
|
||||
end
|
||||
|
||||
-- 从第一个相关库开始请求
|
||||
process_related(1)
|
||||
end)
|
||||
end
|
||||
|
||||
-- 从用户添加过的弹幕源添加弹幕
|
||||
function addon_danmaku(dir, from_menu)
|
||||
if dir then
|
||||
local history_json = read_file(HISTORY_PATH)
|
||||
local history = utils.parse_json(history_json) or {}
|
||||
if history[dir] and history[dir].extra ~= nil then
|
||||
return
|
||||
end
|
||||
end
|
||||
for url, source in pairs(DANMAKU.sources) do
|
||||
if source.from ~= "api_server" then
|
||||
add_danmaku_source(url, from_menu)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--通过输入源url获取弹幕库
|
||||
function add_danmaku_source(query, from_menu)
|
||||
if DANMAKU.sources[query] == nil then
|
||||
DANMAKU.sources[query] = {from = "user_custom"}
|
||||
end
|
||||
|
||||
from_menu = from_menu or false
|
||||
if from_menu then
|
||||
add_source_to_history(query, DANMAKU.sources[query])
|
||||
end
|
||||
|
||||
if is_protocol(query) then
|
||||
add_danmaku_source_online(query, from_menu)
|
||||
else
|
||||
add_danmaku_source_local(query, from_menu)
|
||||
end
|
||||
end
|
||||
|
||||
function add_danmaku_source_local(query, from_menu)
|
||||
local path = normalize(query)
|
||||
if not file_exists(path) then
|
||||
msg.warn("无效的文件路径")
|
||||
return
|
||||
end
|
||||
if not (string.match(path, "%.xml$") or string.match(path, "%.json$") or string.match(path, "%.ass$")) then
|
||||
msg.warn("仅支持弹幕文件")
|
||||
return
|
||||
end
|
||||
|
||||
if DANMAKU.sources[query] ~= nil then
|
||||
if DANMAKU.sources[query].fname and file_exists(DANMAKU.sources[query].fname) then
|
||||
os.remove(DANMAKU.sources[query].fname)
|
||||
end
|
||||
DANMAKU.sources[query]["from"] = "user_local"
|
||||
DANMAKU.sources[query]["fname"] = path
|
||||
else
|
||||
DANMAKU.sources[query] = {from = "user_local", fname = path}
|
||||
end
|
||||
|
||||
set_danmaku_button()
|
||||
load_danmaku(from_menu)
|
||||
end
|
||||
|
||||
--通过输入源url获取弹幕库
|
||||
function add_danmaku_source_online(query, from_menu)
|
||||
set_danmaku_button()
|
||||
local url = options.api_server .. "/api/v2/extcomment?url=" .. url_encode(query)
|
||||
show_message("弹幕加载中...", 30)
|
||||
msg.verbose("尝试获取弹幕:" .. url)
|
||||
local args = make_danmaku_request_args("GET", url)
|
||||
|
||||
if args == nil then
|
||||
return
|
||||
end
|
||||
|
||||
fetch_danmaku_data(args, function(data)
|
||||
if not data or not data["comments"] then
|
||||
show_message("此源弹幕无法加载", 3)
|
||||
msg.verbose("此源弹幕无法加载")
|
||||
return
|
||||
end
|
||||
handle_danmaku_data(query, data, from_menu)
|
||||
end)
|
||||
end
|
||||
|
||||
-- 将弹幕转换为factory可读的json格式
|
||||
function save_danmaku_json(comments, json_filename)
|
||||
local temp_file = "danmaku-" .. PID .. ".json"
|
||||
json_filename = json_filename or utils.join_path(DANMAKU_PATH, temp_file)
|
||||
local json_file = io.open(json_filename, "w")
|
||||
|
||||
if json_file then
|
||||
json_file:write("[\n")
|
||||
for _, comment in ipairs(comments) do
|
||||
local p = comment["p"]
|
||||
local shift = comment["shift"]
|
||||
if p then
|
||||
local fields = split(p, ",")
|
||||
if shift ~= nil then
|
||||
fields[1] = tonumber(fields[1]) + tonumber(shift)
|
||||
end
|
||||
local c_value = string.format(
|
||||
"%s,%s,%s,25,,,",
|
||||
tostring(fields[1]), -- first field of p to first field of c
|
||||
fields[3], -- third field of p to second field of c
|
||||
fields[2] -- second field of p to third field of c
|
||||
)
|
||||
local m_value = comment["m"]
|
||||
:gsub("[%z\1-\31]", "")
|
||||
:gsub("\\", "")
|
||||
:gsub("\"", "")
|
||||
|
||||
-- Write the JSON object as a single line, no spaces or extra formatting
|
||||
local json_entry = string.format('{"c":"%s","m":"%s"},\n', c_value, m_value)
|
||||
json_file:write(json_entry)
|
||||
end
|
||||
end
|
||||
json_file:write("]")
|
||||
json_file:close()
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
-- 通过文件前 16M 的 hash 值进行弹幕匹配
|
||||
function get_danmaku_with_hash(file_name, file_path)
|
||||
if type(MD5) ~= "table" or not MD5.sum then
|
||||
msg.warn("MD5 模块不支持 Lua 5.1,回退到文件名匹配")
|
||||
match_anime()
|
||||
return
|
||||
end
|
||||
if is_protocol(file_path) then
|
||||
set_danmaku_button()
|
||||
local temp_file = "temp-" .. PID .. ".mp4"
|
||||
local arg = {
|
||||
"curl",
|
||||
"--connect-timeout",
|
||||
"10",
|
||||
"--max-time",
|
||||
"30",
|
||||
"--range",
|
||||
"0-16777215",
|
||||
"--user-agent",
|
||||
options.user_agent,
|
||||
"--output",
|
||||
utils.join_path(DANMAKU_PATH, temp_file),
|
||||
"-L",
|
||||
file_path,
|
||||
}
|
||||
|
||||
if options.proxy ~= "" then
|
||||
table.insert(arg, '-x')
|
||||
table.insert(arg, options.proxy)
|
||||
end
|
||||
|
||||
call_cmd_async(arg, function(error)
|
||||
async_running = false
|
||||
|
||||
file_path = utils.join_path(DANMAKU_PATH, temp_file)
|
||||
|
||||
match_file(file_path, file_name, function(error)
|
||||
if error then
|
||||
msg.error(error)
|
||||
msg.info("尝试通过解析文件名获取弹幕")
|
||||
match_anime()
|
||||
end
|
||||
end)
|
||||
end)
|
||||
else
|
||||
local dir = get_parent_directory(file_path)
|
||||
local excluded_path = utils.parse_json(options.excluded_path)
|
||||
if PLATFORM == "windows" then
|
||||
for i, path in pairs(excluded_path) do
|
||||
excluded_path[i] = path:gsub("/", "\\")
|
||||
end
|
||||
end
|
||||
if contains_any(excluded_path, dir) then
|
||||
match_anime()
|
||||
return
|
||||
end
|
||||
match_file(file_path, file_name, function(error)
|
||||
if error then
|
||||
msg.error(error)
|
||||
msg.info("尝试通过解析文件名获取弹幕")
|
||||
match_anime()
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,347 @@
|
||||
local utils = require 'mp.utils'
|
||||
local msg = require 'mp.msg'
|
||||
|
||||
local Source = {
|
||||
["b 站"] = "bilibili1",
|
||||
["腾讯"] = "qq",
|
||||
["爱奇艺"] = "qiyi",
|
||||
["优酷"] = "youku",
|
||||
}
|
||||
|
||||
local function load_extra_danmaku(url, episode, number, class, id, site, title, year)
|
||||
local play_url = nil
|
||||
if url:match("^.-%.html") then
|
||||
play_url = url:match("^(.-%.html).*")
|
||||
else
|
||||
play_url = url:gsub("%?bsource=360ogvys$","")
|
||||
end
|
||||
ENABLED = true
|
||||
DANMAKU.anime = title .. " (" .. year .. ")"
|
||||
DANMAKU.episode = "第" .. episode .. "话"
|
||||
DANMAKU.source = site
|
||||
DANMAKU.extra = {
|
||||
id = id,
|
||||
site = site,
|
||||
year = year,
|
||||
class = class,
|
||||
title = title,
|
||||
number = tonumber(number),
|
||||
episodenum = tonumber(episode),
|
||||
}
|
||||
write_history()
|
||||
add_danmaku_source(play_url, true)
|
||||
end
|
||||
|
||||
local function query_tmdb(title, class, menu)
|
||||
local encoded_title = url_encode(title)
|
||||
local url = string.format("https://api.themoviedb.org/3/search/%s?api_key=%s&query=%s&language=zh-CN",
|
||||
class, Base64.decode(options.tmdb_api_key), encoded_title)
|
||||
|
||||
local cmd = {
|
||||
"curl",
|
||||
"-s",
|
||||
"-H", "accept: application/json",
|
||||
url
|
||||
}
|
||||
|
||||
if options.proxy ~= "" then
|
||||
table.insert(cmd, '-x')
|
||||
table.insert(cmd, options.proxy)
|
||||
end
|
||||
|
||||
local res = mp.command_native({
|
||||
name = "subprocess",
|
||||
args = cmd,
|
||||
capture_stdout = true,
|
||||
capture_stderr = true,
|
||||
})
|
||||
|
||||
local data = utils.parse_json(res.stdout)
|
||||
if not res.status or res.status ~= 0 or not data.results or #data.results == 0 then
|
||||
local message = "获取 tmdb 中文数据失败"
|
||||
if uosc_available then
|
||||
update_menu_uosc(menu.type, menu.title, message, menu.footnote, menu.cmd, title)
|
||||
else
|
||||
show_message(message, 3)
|
||||
end
|
||||
msg.error("获取 tmdb 中文数据失败:" .. res.stdout)
|
||||
else
|
||||
if class == "tv" then
|
||||
return data.results[1].name
|
||||
else
|
||||
return data.results[1].title
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function get_number(cat, id, site)
|
||||
local url = string.format("https://api.web.360kan.com/v1/detail?cat=%s&id=%s&site=%s",
|
||||
cat, id, site)
|
||||
|
||||
local cmd = { "curl", "-s", url }
|
||||
local res = mp.command_native({
|
||||
name = "subprocess",
|
||||
args = cmd,
|
||||
capture_stdout = true,
|
||||
capture_stderr = true,
|
||||
})
|
||||
|
||||
if not res.status or res.status ~= 0 then
|
||||
msg.error("Failed to fetch data: " .. (res.stderr or "unknown error"))
|
||||
return nil
|
||||
end
|
||||
|
||||
local result = utils.parse_json(res.stdout)
|
||||
if result and result.data and result.data.allupinfo then
|
||||
return tonumber(result.data.allupinfo[site])
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function get_details(class, id, site, title, year, number, episodenum)
|
||||
local message = episodenum and "查询弹幕中..." or "加载数据中..."
|
||||
local menu_type = "menu_details"
|
||||
local menu_title = "剧集信息"
|
||||
local footnote = "使用 / 打开筛选"
|
||||
if uosc_available and not episodenum then
|
||||
update_menu_uosc(menu_type, menu_title, message, footnote)
|
||||
else
|
||||
show_message(message, 3)
|
||||
end
|
||||
|
||||
local cat = 0
|
||||
if class == "电影" then
|
||||
cat = 1
|
||||
elseif class == "电视剧" then
|
||||
cat = 2
|
||||
-- elseif class == "综艺" then
|
||||
-- cat = 3
|
||||
elseif class == "动漫" then
|
||||
cat = 4
|
||||
end
|
||||
|
||||
if not number and cat ~= 0 then
|
||||
number = get_number(cat, id, site)
|
||||
end
|
||||
if not number or cat == 0 then
|
||||
local message = "无结果"
|
||||
if uosc_available and not episodenum then
|
||||
update_menu_uosc(menu_type, menu_title, message, footnote)
|
||||
else
|
||||
show_message(message, 3)
|
||||
end
|
||||
msg.verbose("无结果")
|
||||
return
|
||||
end
|
||||
|
||||
local url = string.format("https://api.web.360kan.com/v1/detail?cat=%s&id=%s&start=1&end=%s&site=%s",
|
||||
cat, id, number, site)
|
||||
|
||||
local cmd = { "curl", "-s", url }
|
||||
local res = mp.command_native({
|
||||
name = "subprocess",
|
||||
args = cmd,
|
||||
capture_stdout = true,
|
||||
capture_stderr = true,
|
||||
})
|
||||
|
||||
if not res.status or res.status ~= 0 then
|
||||
local message = "无结果"
|
||||
if uosc_available and not episodenum then
|
||||
update_menu_uosc(menu_type, menu_title, message, footnote)
|
||||
else
|
||||
show_message(message, 3)
|
||||
end
|
||||
msg.verbose("无结果")
|
||||
return
|
||||
end
|
||||
|
||||
local result = utils.parse_json(res.stdout)
|
||||
local items = {}
|
||||
if result and result.data and result.data.allepidetail then
|
||||
local data = result.data.allepidetail
|
||||
local playurl, episode = nil, nil
|
||||
if episodenum then
|
||||
for _, item in ipairs(data[site]) do
|
||||
if tonumber(item.playlink_num) == tonumber(episodenum) then
|
||||
playurl = item.url
|
||||
episode = item.playlink_num
|
||||
break
|
||||
end
|
||||
end
|
||||
if playurl then
|
||||
load_extra_danmaku(playurl, episode, number, class, id, site, title, year)
|
||||
return
|
||||
end
|
||||
end
|
||||
for _, item in ipairs(data[site]) do
|
||||
table.insert(items, {
|
||||
title = "第" .. item.playlink_num .. "集",
|
||||
hint = item.playlink_num,
|
||||
value = {
|
||||
"script-message-to",
|
||||
mp.get_script_name(),
|
||||
"add-extra-event",
|
||||
item.url, item.playlink_num, number, class, id, site, title, year
|
||||
},
|
||||
})
|
||||
end
|
||||
end
|
||||
if #items > 0 then
|
||||
if uosc_available and not episodenum then
|
||||
update_menu_uosc(menu_type, menu_title, items, footnote)
|
||||
elseif not episodenum then
|
||||
show_message("", 0)
|
||||
mp.add_timeout(0.1, function()
|
||||
open_menu_select(items)
|
||||
end)
|
||||
end
|
||||
else
|
||||
local message = "无结果"
|
||||
if uosc_available and not episodenum then
|
||||
update_menu_uosc(menu_type, menu_title, message, footnote)
|
||||
else
|
||||
show_message(message, 3)
|
||||
end
|
||||
msg.verbose("无结果")
|
||||
end
|
||||
end
|
||||
|
||||
local function search_query(query, class, menu)
|
||||
local url = string.format("https://api.so.360kan.com/index?force_v=1&kw=%s", query)
|
||||
if class ~= nil then
|
||||
url = url .. "&type=" .. class
|
||||
end
|
||||
local cmd = { "curl", "-s", url }
|
||||
|
||||
local res = mp.command_native({
|
||||
name = "subprocess",
|
||||
args = cmd,
|
||||
capture_stdout = true,
|
||||
capture_stderr = true,
|
||||
})
|
||||
|
||||
if not res.status or res.status ~= 0 then
|
||||
local message = "无结果"
|
||||
if uosc_available then
|
||||
update_menu_uosc(menu.type, menu.title, message, menu.footnote, menu.cmd, query)
|
||||
else
|
||||
show_message(message, 3)
|
||||
end
|
||||
msg.verbose("无结果")
|
||||
return
|
||||
end
|
||||
|
||||
local result = utils.parse_json(res.stdout)
|
||||
local items = {}
|
||||
if result and result.data.longData and result.data.longData.rows then
|
||||
for _, item in ipairs(result.data.longData.rows) do
|
||||
if item.playlinks then
|
||||
for source_name, source_id in pairs(Source) do
|
||||
if item.playlinks[source_id] then
|
||||
table.insert(items, {
|
||||
title = item.titleTxt,
|
||||
hint = item.cat_name .. " | " .. item.year .. " | 来源:" .. source_name,
|
||||
value = {
|
||||
"script-message-to",
|
||||
mp.get_script_name(),
|
||||
"get-extra-event",
|
||||
item.cat_name, item.en_id, item.playlinks[source_id], source_id,
|
||||
item.titleTxt, item.year,
|
||||
},
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if #items > 0 then
|
||||
if uosc_available then
|
||||
update_menu_uosc(menu.type, menu.title, items, menu.footnote, menu.cmd, query)
|
||||
else
|
||||
show_message("", 0)
|
||||
mp.add_timeout(0.1, function()
|
||||
open_menu_select(items)
|
||||
end)
|
||||
end
|
||||
else
|
||||
local message = "无结果"
|
||||
if uosc_available then
|
||||
update_menu_uosc(menu.type, menu.title, message, menu.footnote, menu.cmd, query)
|
||||
else
|
||||
show_message(message, 3)
|
||||
end
|
||||
msg.verbose("无结果")
|
||||
end
|
||||
end
|
||||
|
||||
function query_extra(name, class)
|
||||
local name = name:gsub("%s*%(%d-%)%s*$", "")
|
||||
local title = nil
|
||||
local class = class and class:lower()
|
||||
local message = "加载数据中..."
|
||||
local menu = {
|
||||
type = "menu_anime",
|
||||
title = "在此处输入番剧名称",
|
||||
footnote = "使用enter或ctrl+enter进行搜索"
|
||||
}
|
||||
menu.cmd = { "script-message-to", mp.get_script_name(), "search-anime-event" }
|
||||
if uosc_available then
|
||||
update_menu_uosc(menu.type, menu.title, message, menu.footnote, menu.cmd, name)
|
||||
else
|
||||
show_message(message, 30)
|
||||
end
|
||||
|
||||
if is_chinese(name) then
|
||||
search_query(name, class, menu)
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
if options.tmdb_api_key == "" or #Base64.decode(options.tmdb_api_key) < 32 then
|
||||
local message = "请正确设置 tmdb_api_key 或尝试使用中文搜索"
|
||||
if uosc_available then
|
||||
update_menu_uosc(menu.type, menu.title, message, menu.footnote, menu.cmd, name)
|
||||
else
|
||||
show_message(message, 3)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if class == "dy" then
|
||||
title = query_tmdb(name, "movie", menu)
|
||||
else
|
||||
title = query_tmdb(name, "tv", menu)
|
||||
end
|
||||
|
||||
if title then
|
||||
search_query(title, class, menu)
|
||||
end
|
||||
end
|
||||
|
||||
mp.register_script_message("get-extra-event", function(cat, id, playlink, source_id, title, year)
|
||||
if uosc_available then
|
||||
mp.commandv("script-message-to", "uosc", "close-menu", "menu_anime")
|
||||
end
|
||||
if cat == "电影" then
|
||||
if playlink:match("^.-%.html") then
|
||||
playlink = playlink:match("^(.-%.html).*")
|
||||
else
|
||||
playlink = playlink:gsub("%?bsource=360ogvys$","")
|
||||
end
|
||||
DANMAKU.anime = title .. " (" .. year .. ")"
|
||||
DANMAKU.episode = "电影"
|
||||
DANMAKU.source = source_id
|
||||
write_history()
|
||||
add_danmaku_source(playlink, true)
|
||||
else
|
||||
get_details(cat, id, source_id, title, year)
|
||||
end
|
||||
end)
|
||||
|
||||
mp.register_script_message("add-extra-event", function(url, episode, number, class, id, site, title, year)
|
||||
if uosc_available then
|
||||
mp.commandv("script-message-to", "uosc", "close-menu", "menu_details")
|
||||
end
|
||||
load_extra_danmaku(url, episode, number, class, id, site, title, year)
|
||||
end)
|
||||
Reference in New Issue
Block a user