update
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
local msg = require('mp.msg')
|
||||
local utils = require("mp.utils")
|
||||
|
||||
local function resolve_bahamut_sn(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
|
||||
|
||||
-- 为 bahamut 网站的视频播放加载弹幕
|
||||
function load_danmaku_for_bahamut(path, callback)
|
||||
callback = callback or function() end
|
||||
local path = path:gsub('%%(%x%x)', hex_to_char)
|
||||
local sn = resolve_bahamut_sn(path)
|
||||
if sn == nil then
|
||||
callback(false)
|
||||
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)
|
||||
if error then
|
||||
show_message("HTTP 请求失败,打开控制台查看详情", 5)
|
||||
msg.error(error)
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
if not file_exists(danmaku_json) then
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
|
||||
local comments_json = read_file(danmaku_json)
|
||||
os.remove(danmaku_json)
|
||||
local comments = utils.parse_json(comments_json)
|
||||
if not comments then
|
||||
callback(false)
|
||||
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)
|
||||
save_danmaku_json("https://ani.gamer.com.tw/animeVideo.php?sn=" .. sn, final_json_str)
|
||||
load_danmaku(true)
|
||||
callback(true)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,246 @@
|
||||
local msg = require('mp.msg')
|
||||
local utils = require("mp.utils")
|
||||
|
||||
local 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'
|
||||
|
||||
local function build_curl_args(url, extra_headers)
|
||||
local args = {
|
||||
'curl', '-L', '-s', '--compressed', '--user-agent', user_agent
|
||||
}
|
||||
extra_headers = extra_headers or {}
|
||||
for _, h in ipairs(extra_headers) do
|
||||
table.insert(args, '-H')
|
||||
table.insert(args, h)
|
||||
end
|
||||
if options.cookie_file and options.cookie_file ~= '' then
|
||||
table.insert(args, '-b')
|
||||
table.insert(args, mp.command_native({'expand-path', options.cookie_file}))
|
||||
end
|
||||
if options.proxy and options.proxy ~= '' then
|
||||
table.insert(args, '-x')
|
||||
table.insert(args, options.proxy)
|
||||
end
|
||||
table.insert(args, url)
|
||||
return args
|
||||
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 get_bilibili_id_and_page(path)
|
||||
local bvid, aid, page = nil, nil, nil
|
||||
if not bvid then
|
||||
bvid = path:match("/video/(BV[%w]+)")
|
||||
or path:match("[?&]bvid=(BV[%w]+)")
|
||||
end
|
||||
if not aid then
|
||||
aid = path:match("/video/av([%d]+)")
|
||||
end
|
||||
if not page then
|
||||
page = tonumber(path:match("[?&]p=(%d+)"))
|
||||
end
|
||||
return bvid, aid, page or 1
|
||||
end
|
||||
|
||||
local function get_bilibili_pagelist_args(bvid, aid)
|
||||
local url
|
||||
if bvid ~= nil then
|
||||
url = "https://api.bilibili.com/x/player/pagelist?bvid=" .. bvid
|
||||
else
|
||||
url = "https://api.bilibili.com/x/player/pagelist?aid=" .. aid
|
||||
end
|
||||
local headers = {"Referer: https://www.bilibili.com/video/" .. (bvid or ("av" .. aid))}
|
||||
return build_curl_args(url, headers)
|
||||
end
|
||||
|
||||
local function resolve_bilibili_cid(path, callback)
|
||||
-- 扩展支持:普通视频(BV/av)、番剧(ep)、课程(cheese)
|
||||
local api_bangumi_season = "https://api.bilibili.com/pgc/view/web/season"
|
||||
local api_cheese_season = "https://api.bilibili.com/pugv/view/web/season"
|
||||
|
||||
local q = path
|
||||
-- 解析普通投稿视频
|
||||
local bvid, aid, p = get_bilibili_id_and_page(q)
|
||||
if bvid or aid then
|
||||
local args = get_bilibili_pagelist_args(bvid, aid)
|
||||
call_cmd_async(args, function(error, json)
|
||||
if error then
|
||||
msg.warn("Failed to request bilibili pagelist: " .. tostring(error))
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local data = utils.parse_json(json)
|
||||
local pages = data and data["data"]
|
||||
if type(pages) ~= "table" then
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local page_info = pages[p] or pages[1]
|
||||
local cid = page_info and page_info["cid"]
|
||||
local part = page_info and page_info["part"]
|
||||
callback(cid and tostring(cid) or nil, part and tostring(part) or nil)
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
-- 番剧、番外等(含 ep)
|
||||
if q:find("bangumi/") and q:find("ep") then
|
||||
local epid = q:match("ep(%d+)") or q:match("ep(%d+)$")
|
||||
if not epid then
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local url = api_bangumi_season .. "?ep_id=" .. epid
|
||||
local arg = build_curl_args(url)
|
||||
call_cmd_async(arg, function(error, json)
|
||||
if error then
|
||||
msg.warn("Failed to request bilibili bangumi info: " .. tostring(error))
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local data = utils.parse_json(json)
|
||||
if not data or data.code ~= 0 or not data.result then
|
||||
msg.warn("bilibili bangumi api returned error")
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
-- 查找正片
|
||||
local episodes = data.result.episodes or {}
|
||||
for _, ep in ipairs(episodes) do
|
||||
if tostring(ep.id) == tostring(epid) then
|
||||
callback(tostring(ep.cid), tostring(ep.share_copy or ep.title or ""))
|
||||
return
|
||||
end
|
||||
end
|
||||
-- 查找 section(花絮等)
|
||||
if type(data.result.section) == "table" then
|
||||
for _, sec in ipairs(data.result.section) do
|
||||
if sec.episodes then
|
||||
for _, ep in ipairs(sec.episodes) do
|
||||
if tostring(ep.id) == tostring(epid) then
|
||||
callback(tostring(ep.cid), tostring(ep.share_copy or ep.title or ""))
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
callback(nil)
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
-- cheese 课程
|
||||
if q:find("cheese/") and q:find("ep") then
|
||||
local epid = q:match("ep(%d+)") or q:match("ep(%d+)$")
|
||||
if not epid then
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local url = api_cheese_season .. "?ep_id=" .. epid
|
||||
local arg = build_curl_args(url)
|
||||
call_cmd_async(arg, function(error, json)
|
||||
if error then
|
||||
msg.warn("Failed to request bilibili cheese info: " .. tostring(error))
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local data = utils.parse_json(json)
|
||||
if not data or data.code ~= 0 or not data.data then
|
||||
msg.warn("bilibili cheese api returned error")
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local episodes = data.data.episodes or {}
|
||||
for _, ep in ipairs(episodes) do
|
||||
if tostring(ep.id) == tostring(epid) then
|
||||
callback(tostring(ep.cid), tostring(ep.title or ""))
|
||||
return
|
||||
end
|
||||
end
|
||||
callback(nil)
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
-- 其它情况返回 nil
|
||||
callback(nil)
|
||||
end
|
||||
|
||||
local function download_bilibili_danmaku(path, cid, from_menu, callback)
|
||||
local url = "https://comment.bilibili.com/" .. cid .. ".xml"
|
||||
local args = build_curl_args(url)
|
||||
|
||||
call_cmd_async(args, function(error, out)
|
||||
if error then
|
||||
show_message("HTTP request failed, see console for details", 5)
|
||||
msg.error(error)
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
if not out or out == '' then
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
save_danmaku_xml(path, out)
|
||||
load_danmaku(from_menu == nil and true or from_menu)
|
||||
callback(true)
|
||||
end)
|
||||
end
|
||||
|
||||
-- 为 bilibli 网站的视频播放加载弹幕
|
||||
function load_danmaku_for_bilibili(path, callback)
|
||||
callback = callback or function() end
|
||||
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 then
|
||||
resolve_bilibili_cid(path, function(resolved_cid)
|
||||
if resolved_cid then
|
||||
download_bilibili_danmaku(path, resolved_cid, true, callback)
|
||||
else
|
||||
show_message("获取哔哩哔哩视频cid失败", 3)
|
||||
msg.error("获取哔哩哔哩视频cid失败")
|
||||
callback(false)
|
||||
end
|
||||
end)
|
||||
return
|
||||
end
|
||||
if cid ~= nil then
|
||||
download_bilibili_danmaku(path, cid, true, callback)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,290 @@
|
||||
local msg = require('mp.msg')
|
||||
local utils = require('mp.utils')
|
||||
local inflate = require('modules/inflate')
|
||||
|
||||
local 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'
|
||||
|
||||
local function build_curl_args(url, extra_headers)
|
||||
local args = {
|
||||
'curl', '-L', '-s', '--compressed', '--user-agent', user_agent
|
||||
}
|
||||
extra_headers = extra_headers or {}
|
||||
for _, h in ipairs(extra_headers) do
|
||||
table.insert(args, '-H')
|
||||
table.insert(args, h)
|
||||
end
|
||||
table.insert(args, url)
|
||||
if options.cookie_file and options.cookie_file ~= '' then
|
||||
table.insert(args, '-b')
|
||||
table.insert(args, mp.command_native({'expand-path', options.cookie_file}))
|
||||
end
|
||||
return args
|
||||
end
|
||||
|
||||
local function get_tvid_from_url(url, callback)
|
||||
local id = url:match('v_(%w+)')
|
||||
if not id then
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local api = string.format('https://pcw-api.iq.com/api/decode/%s?platformId=3&modeCode=intl&langCode=sg', id)
|
||||
call_cmd_async(build_curl_args(api), function(err, out)
|
||||
if err or not out or out == '' then
|
||||
msg.warn('Iqiyi decode request failed: ' .. tostring(err))
|
||||
return
|
||||
end
|
||||
local data = utils.parse_json(out)
|
||||
if not data or not data['data'] then
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local raw = data['data']
|
||||
local tvid_str
|
||||
if type(raw) == 'number' then
|
||||
tvid_str = string.format('%.0f', raw)
|
||||
else
|
||||
tvid_str = tostring(raw)
|
||||
end
|
||||
callback(tvid_str)
|
||||
end)
|
||||
end
|
||||
|
||||
local function get_video_info(tvid, callback)
|
||||
local api = string.format('https://pcw-api.iqiyi.com/video/video/baseinfo/%s', tvid)
|
||||
call_cmd_async(build_curl_args(api), function(err, out)
|
||||
if err or not out or out == '' then
|
||||
msg.warn('Iqiyi baseinfo request failed: ' .. tostring(err))
|
||||
return
|
||||
end
|
||||
local data = utils.parse_json(out)
|
||||
callback(data and data['data'] or nil)
|
||||
end)
|
||||
end
|
||||
|
||||
local function extract_tags(xml, tag)
|
||||
local res = {}
|
||||
if not xml then return res end
|
||||
for v in xml:gmatch('<'..tag..'>(.-)</'..tag..'>') do
|
||||
table.insert(res, v)
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
local function parse_xml_and_append(xml, contents, total_files)
|
||||
local danmaku = extract_tags(xml, 'content')
|
||||
local showTime = extract_tags(xml, 'showTime')
|
||||
local color = extract_tags(xml, 'color')
|
||||
|
||||
if #danmaku == 0 then return end
|
||||
local step = math.ceil(#danmaku * (total_files or 1) / 10000)
|
||||
if step < 1 then step = 1 end
|
||||
|
||||
for i = 1, #danmaku, step do
|
||||
local content = {}
|
||||
local timepoint = tonumber(showTime[i]) or 0
|
||||
local col = tonumber((color[i] or ''), 16) or 16777215
|
||||
local txt = danmaku[i] or ''
|
||||
content.c = string.format('%s,%s,%s,25,,,', timepoint, col, 1)
|
||||
content.m = txt
|
||||
table.insert(contents, content)
|
||||
end
|
||||
end
|
||||
|
||||
local function try_decompress_file(path)
|
||||
local f, err = io.open(path, 'rb')
|
||||
if not f then return nil, err end
|
||||
local data = f:read('*a')
|
||||
f:close()
|
||||
if not data or #data == 0 then return nil, 'empty' end
|
||||
local b1,b2 = data:byte(1,2)
|
||||
-- zip archive (PK..)
|
||||
if b1 == 0x50 and b2 == 0x4b then
|
||||
local bs = inflate.new(data)
|
||||
if bs then
|
||||
local it = bs:files()
|
||||
local name = it()
|
||||
if name then
|
||||
local res = bs:unzip(name, true)
|
||||
if res then return res end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- gzip
|
||||
if b1 == 0x1f and b2 == 0x8b then
|
||||
local function parse_gzip_start(d)
|
||||
local flg = d:byte(4) or 0
|
||||
local pos = 11
|
||||
if (flg % 8) >= 4 then
|
||||
local xlen = (d:byte(11) or 0) + ((d:byte(12) or 0) * 256)
|
||||
pos = pos + 2 + xlen
|
||||
end
|
||||
if (flg % 16) >= 8 then
|
||||
while d:byte(pos) and d:byte(pos) ~= 0 do pos = pos + 1 end
|
||||
pos = pos + 1
|
||||
end
|
||||
if (flg % 32) >= 16 then
|
||||
while d:byte(pos) and d:byte(pos) ~= 0 do pos = pos + 1 end
|
||||
pos = pos + 1
|
||||
end
|
||||
if (flg % 4) >= 2 then
|
||||
pos = pos + 2
|
||||
end
|
||||
return pos
|
||||
end
|
||||
local start = parse_gzip_start(data)
|
||||
if start and start < #data - 8 then
|
||||
local deflated = data:sub(start, #data - 8)
|
||||
local bs = inflate.new(deflated)
|
||||
if bs then
|
||||
local res = bs:inflate(1)
|
||||
if res then return res end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- zlib (check header checksum mod31)
|
||||
if ((b1*256 + (b2 or 0)) % 31) == 0 and #data > 6 then
|
||||
local deflated = data:sub(3, #data - 4)
|
||||
local bs = inflate.new(deflated)
|
||||
if bs then
|
||||
local res = bs:inflate(1)
|
||||
if res then return res end
|
||||
end
|
||||
end
|
||||
-- fallback: return raw data
|
||||
return data
|
||||
end
|
||||
|
||||
local function save_output_and_load(output_table, source_url)
|
||||
if #output_table == 0 then
|
||||
show_message('未获取到任何弹幕', 3)
|
||||
return
|
||||
end
|
||||
local final_json_str = utils.format_json(output_table)
|
||||
save_danmaku_json(source_url, final_json_str)
|
||||
load_danmaku(true)
|
||||
end
|
||||
|
||||
-- 辅助函数:构造 curl 参数
|
||||
local function build_args_for_server(server, referer)
|
||||
local headers = {
|
||||
'Referer: ' .. (referer or ''),
|
||||
'Accept: */*',
|
||||
}
|
||||
local args = build_curl_args(server.url, headers)
|
||||
local last = args[#args]
|
||||
args[#args] = '--output'
|
||||
table.insert(args, server.zfile)
|
||||
table.insert(args, last)
|
||||
return args
|
||||
end
|
||||
|
||||
-- 辅助函数:处理单个分段的响应并解压解析
|
||||
local function handle_server_response(server, err, out, output_table, servers, referer)
|
||||
if err then
|
||||
msg.warn('请求弹幕段失败: ' .. tostring(server) .. ' 错误: ' .. tostring(err))
|
||||
return
|
||||
end
|
||||
|
||||
if type(out) == 'string' and out:find('<') then
|
||||
parse_xml_and_append(out, output_table, #servers)
|
||||
return
|
||||
end
|
||||
|
||||
if type(server) == 'table' and server.zfile and utils.file_info(server.zfile) then
|
||||
local content, derr = try_decompress_file(server.zfile)
|
||||
if not content then
|
||||
msg.warn('文件解压失败: ' .. tostring(server.url) .. ' 错误: ' .. tostring(derr))
|
||||
os.remove(server.zfile)
|
||||
return
|
||||
end
|
||||
if not content or content == '' then
|
||||
msg.warn('文件解压后为空: ' .. tostring(server.url))
|
||||
os.remove(server.zfile)
|
||||
return
|
||||
end
|
||||
if type(content) == 'string' and content:find('<') then
|
||||
parse_xml_and_append(content, output_table, #servers)
|
||||
else
|
||||
msg.warn('无法解析弹幕段内容: ' .. tostring(server.url))
|
||||
end
|
||||
os.remove(server.zfile)
|
||||
return
|
||||
end
|
||||
msg.warn('无法处理弹幕段响应: ' .. tostring(server))
|
||||
end
|
||||
|
||||
-- 辅助函数:处理已知 tvid 的主流程
|
||||
local function process_iqiyi_with_tvid(tvid, url, callback)
|
||||
get_video_info(tvid, function(videoInfo)
|
||||
if not videoInfo then
|
||||
show_message('获取爱奇艺视频信息失败', 3)
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
|
||||
local title = videoInfo['name'] or videoInfo['tvName'] or ''
|
||||
local duration = tonumber(videoInfo['durationSec']) or 0
|
||||
local albumid = videoInfo['albumId']
|
||||
local categoryid = videoInfo['channelId'] or videoInfo['categoryId']
|
||||
if title and title ~= '' then
|
||||
DANMAKU.title = title
|
||||
end
|
||||
|
||||
local page = math.ceil(duration / (60 * 5))
|
||||
if page < 1 then page = 1 end
|
||||
msg.verbose(string.format('tvid: %s duration: %s pages: %d', tvid, tostring(duration), page))
|
||||
|
||||
local servers = {}
|
||||
for i = 0, page - 1 do
|
||||
local part1 = tvid:sub(-4, -3) or ''
|
||||
local part2 = tvid:sub(-2) or ''
|
||||
local api_url = string.format('https://cmts.iqiyi.com/bullet/%s/%s/%s_300_%d.z', part1, part2, tvid, i + 1)
|
||||
local qs = '?rn=0.0123456789123456&business=danmu&is_iqiyi=true&is_video_page=true&tvid='..url_encode(tvid)
|
||||
if albumid then qs = qs .. '&albumid=' .. url_encode(tostring(albumid)) end
|
||||
if categoryid then qs = qs .. '&categoryid=' .. url_encode(tostring(categoryid)) end
|
||||
qs = qs .. '&qypid=01010021010000000000'
|
||||
local full = api_url .. qs
|
||||
local tmp_file = 'iqiyi_' .. PID .. '_' .. tostring(os.time()) .. '_' .. tostring(i) .. '.z'
|
||||
local tmp_path = utils.join_path(DANMAKU_PATH, tmp_file)
|
||||
table.insert(servers, { url = full, zfile = tmp_path, idx = i + 1 })
|
||||
end
|
||||
|
||||
local output_table = {}
|
||||
|
||||
local function build_args_fn(server)
|
||||
return build_args_for_server(server, url)
|
||||
end
|
||||
|
||||
local function per_response_cb(server, err, out)
|
||||
return handle_server_response(server, err, out, output_table, servers, url)
|
||||
end
|
||||
|
||||
local function final_cb()
|
||||
local ok = #output_table > 0
|
||||
save_output_and_load(output_table, url)
|
||||
callback(ok)
|
||||
end
|
||||
|
||||
parallel_requests(servers, build_args_fn, per_response_cb, final_cb, {concurrency = 6, per_request_timeout = 15})
|
||||
end)
|
||||
end
|
||||
|
||||
-- 为爱奇艺加载弹幕
|
||||
function load_danmaku_for_iqiyi(path, callback)
|
||||
callback = callback or function() end
|
||||
local url = path or mp.get_property('stream-open-filename', '')
|
||||
if not url or url == '' then
|
||||
msg.error('无有效 URL')
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
|
||||
get_tvid_from_url(url, function(tvid)
|
||||
if not tvid then
|
||||
show_message('获取爱奇艺 tvid 失败', 3)
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
process_iqiyi_with_tvid(tvid, url, callback)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,146 @@
|
||||
local msg = require('mp.msg')
|
||||
local utils = require('mp.utils')
|
||||
|
||||
local 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'
|
||||
|
||||
-- 解析多种 time 表示为秒数(支持数字、带小数、HH:MM:SS、MM:SS)
|
||||
local function parse_mgtv_time(time_str)
|
||||
if time_str == nil then return 0 end
|
||||
if type(time_str) == 'number' then return time_str end
|
||||
local s = tostring(time_str):gsub('^%s*(.-)%s*$', '%1')
|
||||
local n = tonumber(s)
|
||||
if n then return n end
|
||||
local h, m, sec = s:match('^(%d+):(%d+):([%d%.]+)$')
|
||||
if h and m and sec then
|
||||
return tonumber(h) * 3600 + tonumber(m) * 60 + tonumber(sec)
|
||||
end
|
||||
local mm, ss = s:match('^(%d+):([%d%.]+)$')
|
||||
if mm and ss then
|
||||
return tonumber(mm) * 60 + tonumber(ss)
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
-- 生成分段请求列表(每段以 ms 为单位的起点)
|
||||
local function generate_mgtv_segments(api_base, total_seconds, step_ms)
|
||||
local segments = {}
|
||||
local end_time_ms = math.floor(total_seconds * 1000)
|
||||
for i = 0, math.max(0, end_time_ms - 1), step_ms do
|
||||
table.insert(segments, api_base .. tostring(i))
|
||||
end
|
||||
return segments
|
||||
end
|
||||
|
||||
-- 构建通用 curl 请求参数
|
||||
local function build_mgtv_curl_args(target_url)
|
||||
local args = {
|
||||
'curl', '-s', '-L', '--compressed',
|
||||
'--user-agent', user_agent,
|
||||
'-H', 'Accept: application/json',
|
||||
target_url,
|
||||
}
|
||||
return args
|
||||
end
|
||||
|
||||
-- 解析单个分段返回并把弹幕追加到 output_table
|
||||
local function parse_mgtv_segment(out, output_table)
|
||||
if not out then return end
|
||||
local j = utils.parse_json(out)
|
||||
if not j or not j.data or not j.data.items then return end
|
||||
for _, item in ipairs(j.data.items) do
|
||||
local t_ms = tonumber(item.time) or 0
|
||||
local time_s = t_ms / 1000
|
||||
local content = item.content or ''
|
||||
local mode = 1
|
||||
local color = 16777215
|
||||
local c_param = string.format('%.2f,%d,%d,25,,,', time_s, color, mode)
|
||||
table.insert(output_table, { c = c_param, m = content })
|
||||
end
|
||||
end
|
||||
|
||||
local function extract_mgtv_ids(path)
|
||||
if not path then return nil, nil end
|
||||
-- 常见格式: /b/<cid>/<vid>.html
|
||||
local cid, vid = path:match('/b/(%d+)/([%w%._-]+)%.html')
|
||||
if cid and vid then
|
||||
vid = vid:match('([^.]+)') or vid
|
||||
return cid, vid
|
||||
end
|
||||
|
||||
-- 回退:取最后两个 path segment
|
||||
local segs = {}
|
||||
for seg in path:gmatch('/([^/]+)') do table.insert(segs, seg) end
|
||||
if #segs >= 2 then
|
||||
cid = segs[#segs - 1]
|
||||
vid = segs[#segs]
|
||||
vid = vid and vid:match('([^.]+)') or nil
|
||||
return cid, vid
|
||||
end
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
-- 为 芒果TV 加载弹幕
|
||||
function load_danmaku_for_mgtv(path, callback)
|
||||
callback = callback or function() end
|
||||
local url = path or mp.get_property('stream-open-filename', '')
|
||||
if not url or url == '' then
|
||||
msg.error('mgtv: 无效的 url')
|
||||
return
|
||||
end
|
||||
|
||||
local cid, vid = extract_mgtv_ids(url)
|
||||
if not cid or not vid then
|
||||
msg.error('mgtv: 无法解析 cid/vid: ' .. tostring(url))
|
||||
return
|
||||
end
|
||||
|
||||
local info_api = 'https://pcweb.api.mgtv.com/video/info?cid=' .. cid .. '&vid=' .. vid
|
||||
local api_danmaku_base = 'https://galaxy.bz.mgtv.com/rdbarrage?vid=' .. vid .. '&cid=' .. cid .. '&time='
|
||||
|
||||
local args = build_mgtv_curl_args(info_api)
|
||||
|
||||
call_cmd_async(args, function(err, out)
|
||||
if err then
|
||||
msg.error('mgtv: 请求 video/info 失败: ' .. tostring(err))
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
local data = utils.parse_json(out)
|
||||
if not data or data.code ~= 200 or not data.data or not data.data.info then
|
||||
msg.info('mgtv: video/info 返回无效')
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
local time_str = data.data.info.time
|
||||
local total_seconds = parse_mgtv_time(time_str)
|
||||
|
||||
local step = 60 * 1000
|
||||
local segments = generate_mgtv_segments(api_danmaku_base, total_seconds, step)
|
||||
|
||||
if #segments == 0 then
|
||||
msg.info('mgtv: 未生成任何弹幕分段请求')
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
|
||||
local output_table = {}
|
||||
|
||||
local function per_response_cb(server, err, out)
|
||||
if err then
|
||||
msg.debug('mgtv segment request failed: ' .. tostring(server) .. ' err: ' .. tostring(err))
|
||||
return
|
||||
end
|
||||
parse_mgtv_segment(out, output_table)
|
||||
end
|
||||
|
||||
local function final_cb()
|
||||
local ok = #output_table > 0
|
||||
local final_json_str = utils.format_json(output_table)
|
||||
save_danmaku_json(url, final_json_str)
|
||||
load_danmaku(true)
|
||||
callback(ok)
|
||||
end
|
||||
|
||||
parallel_requests(segments, build_mgtv_curl_args, per_response_cb, final_cb, { concurrency = 6, per_request_timeout = 10 })
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,157 @@
|
||||
local msg = require('mp.msg')
|
||||
local utils = require('mp.utils')
|
||||
|
||||
local 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'
|
||||
|
||||
-- 将 URL 中的百分号编码解码为字符
|
||||
local function normalize_url(path)
|
||||
if not path then return '' end
|
||||
return (path:gsub('%%(%x%x)', hex_to_char))
|
||||
end
|
||||
|
||||
-- 从 URL 提取 vid
|
||||
local function extract_vid(url)
|
||||
if not url then return nil end
|
||||
local vid = url:match('[?&]vid=([^&?#]+)')
|
||||
if not vid then
|
||||
local last = nil
|
||||
for seg in url:gmatch('/([^/?#]+)') do
|
||||
last = seg
|
||||
end
|
||||
if last then
|
||||
vid = last:match('([^%.]+)')
|
||||
end
|
||||
end
|
||||
return vid
|
||||
end
|
||||
|
||||
-- 构造 curl 请求参数(通用)
|
||||
local function build_curl_args(target_url)
|
||||
local args = {
|
||||
'curl',
|
||||
'-L',
|
||||
'-s',
|
||||
'--compressed',
|
||||
'--user-agent',
|
||||
user_agent,
|
||||
target_url,
|
||||
}
|
||||
|
||||
if options.cookie_file and options.cookie_file ~= '' then
|
||||
table.insert(args, '-b')
|
||||
table.insert(args, mp.command_native({'expand-path', options.cookie_file}))
|
||||
end
|
||||
return args
|
||||
end
|
||||
|
||||
-- 解析单个 segment 返回并把弹幕追加到 output_table
|
||||
local function parse_segment_to_output(seg_json, output_table)
|
||||
if not seg_json or not seg_json['barrage_list'] then return end
|
||||
for _, item in ipairs(seg_json['barrage_list']) do
|
||||
local time = tonumber(item['time_offset']) and tonumber(item['time_offset']) / 1000 or 0
|
||||
local color = 16777215
|
||||
if item['content_style'] and item['content_style']['color'] then
|
||||
local col = item['content_style']['color']
|
||||
if type(col) == 'string' and col:match('^#') then
|
||||
color = hex_to_int_color(col)
|
||||
end
|
||||
end
|
||||
local mode = 1
|
||||
local c_param = string.format('%s,%s,%s,25,,,', time, color, mode)
|
||||
table.insert(output_table, {c = c_param, m = item['content'] or ''})
|
||||
end
|
||||
end
|
||||
|
||||
-- 保存并加载最终弹幕 JSON
|
||||
local function save_output_and_load(output_table, source_url)
|
||||
if #output_table == 0 then
|
||||
show_message('未获取到任何弹幕', 3)
|
||||
return
|
||||
end
|
||||
local final_json_str = utils.format_json(output_table)
|
||||
save_danmaku_json(source_url, final_json_str)
|
||||
load_danmaku(true)
|
||||
end
|
||||
|
||||
-- 为 腾讯视频 加载弹幕
|
||||
function load_danmaku_for_tencent(path, callback)
|
||||
callback = callback or function() end
|
||||
local url = normalize_url(path)
|
||||
if not url or url == '' then
|
||||
url = mp.get_property('stream-open-filename', '')
|
||||
end
|
||||
|
||||
local vid = extract_vid(url)
|
||||
if not vid then
|
||||
msg.error('无法从 URL 中解析 vid: ' .. tostring(url))
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
|
||||
local api_base = 'https://dm.video.qq.com/barrage/base/' .. vid
|
||||
local api_segment_base = 'https://dm.video.qq.com/barrage/segment/' .. vid .. '/'
|
||||
|
||||
local base_args = build_curl_args(api_base)
|
||||
|
||||
call_cmd_async(base_args, function(err, out)
|
||||
if err then
|
||||
msg.error('请求腾讯弹幕 base 失败: ' .. tostring(err))
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
|
||||
local base_json = utils.parse_json(out)
|
||||
if not base_json or not base_json['segment_index'] then
|
||||
show_message('好像没有弹幕哦', 3)
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
|
||||
-- 构造 segment 请求列表
|
||||
local segments = {}
|
||||
local seg_index = base_json['segment_index']
|
||||
if type(seg_index) == 'table' then
|
||||
for k, v in pairs(seg_index) do
|
||||
local seg_name = nil
|
||||
if type(v) == 'table' and v['segment_name'] then
|
||||
seg_name = v['segment_name']
|
||||
elseif type(k) == 'string' then
|
||||
seg_name = k
|
||||
end
|
||||
if seg_name then
|
||||
table.insert(segments, api_segment_base .. seg_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if #segments == 0 then
|
||||
show_message('没有找到弹幕分段', 3)
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
|
||||
local output_table = {}
|
||||
|
||||
local function build_args_fn(server)
|
||||
return build_curl_args(server)
|
||||
end
|
||||
|
||||
local function per_response_cb(server, err, out)
|
||||
if err then
|
||||
msg.warn('请求段失败: ' .. tostring(server) .. ' 错误: ' .. tostring(err))
|
||||
return
|
||||
end
|
||||
local seg_json = utils.parse_json(out)
|
||||
parse_segment_to_output(seg_json, output_table)
|
||||
end
|
||||
|
||||
local function final_cb()
|
||||
local ok = #output_table > 0
|
||||
save_output_and_load(output_table, url)
|
||||
callback(ok)
|
||||
end
|
||||
|
||||
-- 并行请求 segments
|
||||
parallel_requests(segments, build_args_fn, per_response_cb, final_cb, {concurrency = 6, per_request_timeout = 15})
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,323 @@
|
||||
local msg = require('mp.msg')
|
||||
local utils = require('mp.utils')
|
||||
|
||||
local 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'
|
||||
|
||||
local function build_curl_args(url, extra_headers)
|
||||
local args = {'curl', '-L', '-s', '--compressed', '--user-agent', user_agent}
|
||||
extra_headers = extra_headers or {}
|
||||
for _, h in ipairs(extra_headers) do
|
||||
table.insert(args, '-H')
|
||||
table.insert(args, h)
|
||||
end
|
||||
if options.cookie_file and options.cookie_file ~= '' then
|
||||
table.insert(args, '-b')
|
||||
table.insert(args, mp.command_native({'expand-path', options.cookie_file}))
|
||||
end
|
||||
table.insert(args, url)
|
||||
return args
|
||||
end
|
||||
|
||||
local function parse_set_cookie_headers(text)
|
||||
local cookies = {}
|
||||
if not text or text == '' then return cookies end
|
||||
for line in text:gmatch('[^\r\n]+') do
|
||||
local low = line:lower()
|
||||
if low:find('set%-cookie:') then
|
||||
local cookie_str = line:match(':%s*(.*)') or ''
|
||||
for kv in cookie_str:gmatch('([^;]+)') do
|
||||
local k, v = kv:match('^%s*(.-)%s*=%s*(.*)')
|
||||
if k and v then
|
||||
cookies[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return cookies
|
||||
end
|
||||
|
||||
local function get_cna(callback)
|
||||
local api = 'https://log.mmstat.com/eg.js'
|
||||
local args = build_curl_args(api)
|
||||
table.insert(args, #args, '-i')
|
||||
call_cmd_async(args, function(err, out)
|
||||
if err or not out or out == '' then
|
||||
msg.warn('get_cna failed: ' .. tostring(err))
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local cookies = parse_set_cookie_headers(out)
|
||||
callback(cookies['cna'])
|
||||
end)
|
||||
end
|
||||
|
||||
local function get_tk_enc(callback)
|
||||
local api_url = 'https://acs.youku.com/h5/mtop.com.youku.aplatform.weakget/1.0/?jsv=2.5.1&appKey=24679788'
|
||||
local args = build_curl_args(api_url)
|
||||
table.insert(args, #args, '-i')
|
||||
call_cmd_async(args, function(err, out)
|
||||
if err or not out or out == '' then
|
||||
msg.warn('get_tk_enc failed: ' .. tostring(err))
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local cookies = parse_set_cookie_headers(out)
|
||||
callback(cookies)
|
||||
end)
|
||||
end
|
||||
|
||||
local function yk_msg_sign(msg)
|
||||
return MD5.sum(msg .. 'MkmC9SoIw6xCkSKHhJ7b5D2r51kBiREr')
|
||||
end
|
||||
|
||||
local function yk_t_sign(token, t, appkey, data)
|
||||
local text = table.concat({token, tostring(t), appkey, data}, '&')
|
||||
return MD5.sum(text)
|
||||
end
|
||||
|
||||
local function get_vinfos_by_video_id(url, callback)
|
||||
local vid = url:match('/v_show/id_([%w=]+)') or url:match('id_([%w=]+)') or url:match("[?&]vid=([^&]+)")
|
||||
if not vid then
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local api_url = 'https://openapi.youku.com/v2/videos/show.json'
|
||||
local params = '?client_id=53e6cc67237fc59a&video_id=' .. url_encode(vid) .. '&package=com.huawei.hwvplayer.youku&ext=show'
|
||||
local args = build_curl_args(api_url .. params)
|
||||
call_cmd_async(args, function(err, out)
|
||||
if err or not out or out == '' then
|
||||
msg.warn('youku show.json failed: ' .. tostring(err))
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local data = utils.parse_json(out)
|
||||
if not data then
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local duration = tonumber(data.duration) or 0
|
||||
local title = data.title or ''
|
||||
if title and title ~= '' then DANMAKU.title = title end
|
||||
callback(vid, duration)
|
||||
end)
|
||||
end
|
||||
|
||||
local function build_query_string(params)
|
||||
local parts = {}
|
||||
for k, v in pairs(params) do
|
||||
table.insert(parts, k .. '=' .. url_encode(tostring(v)))
|
||||
end
|
||||
return table.concat(parts, '&')
|
||||
end
|
||||
|
||||
-- Helper: 构造 curl 参数(POST form data)
|
||||
local function youku_build_curl_args_for_server(server)
|
||||
local headers = {
|
||||
'Content-Type: application/x-www-form-urlencoded',
|
||||
'Referer: https://v.youku.com',
|
||||
'User-Agent: ' .. user_agent,
|
||||
'Cookie: ' .. server.cookie
|
||||
}
|
||||
local args = build_curl_args(server.url, headers)
|
||||
table.insert(args, '--data-urlencode')
|
||||
table.insert(args, 'data=' .. server.data)
|
||||
msg.info('youku build args mat=' .. tostring(server.mat) .. ' sign=' .. tostring(server.sign) .. ' msg_b64_prefix=' .. string.sub(server.msg_b64 or '', 1, 16))
|
||||
msg.verbose('youku curl args: ' .. table.concat(args, ' '))
|
||||
return args
|
||||
end
|
||||
|
||||
-- Helper: 解析单个 youku 响应并把 danmu 加入 output_table
|
||||
local function youku_parse_response(out, server, output_table)
|
||||
if not out or out == '' then return 0 end
|
||||
|
||||
-- 优先解析外层 JSON,再解析内层被转义的 result
|
||||
local parsed = nil
|
||||
local outer = utils.parse_json(out)
|
||||
if outer and type(outer) == 'table' then
|
||||
if outer.data then
|
||||
if type(outer.data) == 'string' then
|
||||
local inner = utils.parse_json(outer.data)
|
||||
if inner and type(inner) == 'table' then parsed = inner end
|
||||
elseif type(outer.data) == 'table' then
|
||||
if type(outer.data.result) == 'string' then
|
||||
local inner = utils.parse_json(outer.data.result)
|
||||
if inner and type(inner) == 'table' then parsed = inner end
|
||||
elseif type(outer.data.result) == 'table' then
|
||||
parsed = outer.data
|
||||
end
|
||||
end
|
||||
elseif type(outer.result) == 'string' then
|
||||
local inner = utils.parse_json(outer.result)
|
||||
if inner and type(inner) == 'table' then parsed = inner end
|
||||
elseif type(outer.result) == 'table' then
|
||||
parsed = outer
|
||||
end
|
||||
end
|
||||
|
||||
if not parsed then
|
||||
local s, e = out:find('%b{}')
|
||||
if s and e then
|
||||
local snippet = out:sub(s, e)
|
||||
local sn = utils.parse_json(snippet)
|
||||
if sn and type(sn) == 'table' then parsed = sn end
|
||||
end
|
||||
end
|
||||
|
||||
if not parsed then
|
||||
msg.warn('youku parse failed for response; skipping')
|
||||
return 0
|
||||
end
|
||||
|
||||
if tostring(parsed.code) == '-1' then return 0 end
|
||||
local danmus = parsed.data and parsed.data.result or parsed.result
|
||||
if not danmus then return 0 end
|
||||
|
||||
local added = 0
|
||||
for _, d in ipairs(danmus) do
|
||||
local content = {}
|
||||
local timepoint = tonumber(d.playat) and tonumber(d.playat)/1000 or 0
|
||||
local properties = {}
|
||||
if d.propertis then
|
||||
local ps = utils.parse_json(d.propertis)
|
||||
if ps then properties = ps end
|
||||
end
|
||||
local color = tonumber(properties.color) or 16777215
|
||||
content.c = string.format('%s,%s,%s,25,,,', timepoint, color, 1)
|
||||
content.m = d.content or ''
|
||||
table.insert(output_table, content)
|
||||
added = added + 1
|
||||
end
|
||||
|
||||
return added
|
||||
end
|
||||
|
||||
-- Helper: 最终保存并加载弹幕
|
||||
local function youku_final_save(output_table, url, callback)
|
||||
local ok = #output_table > 0
|
||||
local final_json_str = utils.format_json(output_table)
|
||||
save_danmaku_json(url, final_json_str)
|
||||
load_danmaku(true)
|
||||
callback(ok)
|
||||
end
|
||||
|
||||
-- Helper: 根据 mat/guid/tk/vid 构造单个 server 条目
|
||||
local function youku_make_server_entry(mat, guid, tk, vid)
|
||||
local api_url = 'https://acs.youku.com/h5/mopen.youku.danmu.list/1.0/'
|
||||
local msg_obj = {
|
||||
ctime = os.time() * 1000,
|
||||
ctype = 10004,
|
||||
cver = 'v1.0',
|
||||
guid = guid,
|
||||
mat = mat,
|
||||
mcount = 1,
|
||||
pid = 0,
|
||||
sver = '3.1.0',
|
||||
type = 1,
|
||||
vid = vid
|
||||
}
|
||||
local data_json = utils.format_json(msg_obj)
|
||||
local msg_b64 = Base64.encode(data_json)
|
||||
msg_obj.msg = msg_b64
|
||||
msg_obj.sign = yk_msg_sign(msg_b64)
|
||||
local data_wrapper = utils.format_json(msg_obj)
|
||||
local t = tostring(os.time() * 1000)
|
||||
local token_raw = tk['_m_h5_tk'] or ''
|
||||
local token = token_raw:sub(1, 32)
|
||||
local params = {
|
||||
jsv = '2.5.6',
|
||||
appKey = '24679788',
|
||||
t = t,
|
||||
sign = yk_t_sign(token, t, '24679788', data_wrapper),
|
||||
api = 'mopen.youku.danmu.list',
|
||||
v = '1.0',
|
||||
type = 'originaljson',
|
||||
dataType = 'jsonp',
|
||||
timeout = '20000',
|
||||
jsonpIncPrefix = 'utility'
|
||||
}
|
||||
local qs = build_query_string(params)
|
||||
local full_url = api_url .. '?' .. qs
|
||||
local cookie_header = '_m_h5_tk=' .. (tk['_m_h5_tk'] or '') .. ';_m_h5_tk_enc=' .. (tk['_m_h5_tk_enc'] or '') .. ';'
|
||||
return {url = full_url, data = data_wrapper, cookie = cookie_header, mat = mat, sign = msg_obj.sign, msg_b64 = msg_b64}
|
||||
end
|
||||
|
||||
-- Helper: 并行获取 cna 和 tk,然后调用回调 cb({cna=..., tk=...})
|
||||
local function gather_cna_and_tk(cb)
|
||||
local res = {}
|
||||
local remaining = 2
|
||||
local function maybe_done()
|
||||
remaining = remaining - 1
|
||||
if remaining == 0 then cb(res) end
|
||||
end
|
||||
get_cna(function(cna)
|
||||
res.cna = cna
|
||||
maybe_done()
|
||||
end)
|
||||
get_tk_enc(function(tk)
|
||||
res.tk = tk
|
||||
maybe_done()
|
||||
end)
|
||||
end
|
||||
|
||||
-- Helper: 从已知参数发起 youku 弹幕请求并处理结果
|
||||
local function start_youku_requests(source_url, vid, duration, cna, tk, callback)
|
||||
local max_mat = math.floor(duration / 60) + 1
|
||||
if max_mat < 1 then max_mat = 1 end
|
||||
msg.verbose(string.format('vid: %s duration: %s mats: %d', vid, tostring(duration), max_mat))
|
||||
|
||||
local servers = {}
|
||||
for mat = 0, max_mat - 1 do
|
||||
table.insert(servers, youku_make_server_entry(mat, cna, tk, vid))
|
||||
end
|
||||
|
||||
local output_table = {}
|
||||
|
||||
local function per_response_cb(server, err, out)
|
||||
if err then
|
||||
msg.warn('youku request failed: ' .. tostring(err))
|
||||
return
|
||||
end
|
||||
if not out or out == '' then return end
|
||||
youku_parse_response(out, server, output_table)
|
||||
end
|
||||
|
||||
local function final_cb()
|
||||
youku_final_save(output_table, source_url, callback)
|
||||
end
|
||||
|
||||
parallel_requests(servers, youku_build_curl_args_for_server, per_response_cb, final_cb, {concurrency = 6, per_request_timeout = 20})
|
||||
end
|
||||
|
||||
function load_danmaku_for_youku(path, callback)
|
||||
callback = callback or function() end
|
||||
local url = path or mp.get_property('stream-open-filename', '')
|
||||
if not url or url == '' then
|
||||
msg.error('无有效 URL')
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
|
||||
get_vinfos_by_video_id(url, function(vid, duration)
|
||||
if not vid then
|
||||
show_message('获取优酷 video_id 失败', 3)
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
|
||||
gather_cna_and_tk(function(res)
|
||||
local cna = res.cna
|
||||
local tk = res.tk
|
||||
if not cna then
|
||||
show_message('获取优酷 cna 失败', 3)
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
if not tk then
|
||||
show_message('获取优酷 tk_enc 失败', 3)
|
||||
callback(false)
|
||||
return
|
||||
end
|
||||
start_youku_requests(url, vid, duration, cna, tk, callback)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
Reference in New Issue
Block a user