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