This commit is contained in:
2026-08-15 17:50:48 +02:00
parent 22246060e6
commit 4dc6687de7
35 changed files with 4978 additions and 1509 deletions
+118
View File
@@ -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
+246
View File
@@ -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
+290
View File
@@ -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
+146
View File
@@ -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
+157
View File
@@ -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
+323
View File
@@ -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