// ==UserScript== // @name:en-US CCTV-HLS // @name CCTV视频解析 // @description:en-US parse cctv video to hls url. // @description 将CCTV视频解析成HLS地址. // @namespace https://greasyfork.org/users/135090 // @version 1.6.8.1 // @author [ZWB](https://greasyfork.org/zh-CN/users/863179) // @license CC // @grant none // @run-at document-end // @match *://live.ipanda.com/*/*/*/V*.shtml* // @match *://*.cctv.com/*/*/*/V*.shtml* // @match *://*.cctv.com/*/*/*/A*.shtml* // @match *://*.cctv.cn/*/*/*/V*.shtml* // @match *://*.cctv.cn/*/*/*/A*.shtml* // @match *://*.cntv.cn/program/*/*/*.shtml* // @match *://vdn.apps.cntv.cn/api/getHttpVideoInfo* // @icon https://tv.cctv.cn/favicon.ico // ==/UserScript== 'use strict'; const md5 = (() => { var hex_chr = '0123456789abcdef'.split(''); function rhex(num) { var s = ''; for (var j = 0; j < 4; j++) s += hex_chr[(num >> (j * 8 + 4)) & 0x0F] + hex_chr[(num >> (j * 8)) & 0x0F]; return s; } function add(x, y) { var lsw = (x & 0xFFFF) + (y & 0xFFFF); var msw = (x >> 16) + (y >> 16) + (lsw >> 16); return (msw << 16) | (lsw & 0xFFFF); } function cmn(q, a, b, x, s, t) { return add(add(rol(add(add(a, q), add(x, t)), s), b), 0); } function ff(a, b, c, d, x, s, t) { return cmn((b & c) | ((~b) & d), a, b, x, s, t); } function gg(a, b, c, d, x, s, t) { return cmn((b & d) | (c & (~d)), a, b, x, s, t); } function hh(a, b, c, d, x, s, t) { return cmn(b ^ c ^ d, a, b, x, s, t); } function ii(a, b, c, d, x, s, t) { return cmn(c ^ (b | (~d)), a, b, x, s, t); } function rol(num, cnt) { return (num << cnt) | (num >>> (32 - cnt)); } return function(s) { var x = []; for (var i = 0; i < s.length * 8; i += 8) x[i >> 5] |= (s.charCodeAt(i / 8) & 0xFF) << (i % 32); x[s.length * 8 >> 5] |= 0x80 << ((s.length * 8) % 32); x[(((s.length * 8) + 64) >>> 9 << 4) + 14] = s.length * 8; var a = 1732584193, b = -271733879, c = -1732584194, d = 271733878; for (var i = 0; i < x.length; i += 16) { var olda = a, oldb = b, oldc = c, oldd = d; a = ff(a,b,c,d,x[i],7,-680876936); d = ff(d,a,b,c,x[i+1],12,-389564586); c = ff(c,d,a,b,x[i+2],17,606105819); b = ff(b,c,d,a,x[i+3],22,-1044525330); a = ff(a,b,c,d,x[i+4],7,-176418897); d = ff(d,a,b,c,x[i+5],12,1200080426); c = ff(c,d,a,b,x[i+6],17,-1473231341); b = ff(b,c,d,a,x[i+7],22,-45705983); a = ff(a,b,c,d,x[i+8],7,1770035416); d = ff(d,a,b,c,x[i+9],12,-1958414417); c = ff(c,d,a,b,x[i+10],17,-42063); b = ff(b,c,d,a,x[i+11],22,-1990404162); a = ff(a,b,c,d,x[i+12],7,1804603682); d = ff(d,a,b,c,x[i+13],12,-40341101); c = ff(c,d,a,b,x[i+14],17,-1502002290); b = ff(b,c,d,a,x[i+15],22,1236535329); a = gg(a,b,c,d,x[i+1],5,-165796510); d = gg(d,a,b,c,x[i+6],9,-1069501632); c = gg(c,d,a,b,x[i+11],14,643717713); b = gg(b,c,d,a,x[i],20,-373897302); a = gg(a,b,c,d,x[i+5],5,-701558691); d = gg(d,a,b,c,x[i+10],9,38016083); c = gg(c,d,a,b,x[i+15],14,-660478335); b = gg(b,c,d,a,x[i+4],20,-405537848); a = gg(a,b,c,d,x[i+9],5,568446438); d = gg(d,a,b,c,x[i+14],9,-1019803690); c = gg(c,d,a,b,x[i+3],14,-187363961); b = gg(b,c,d,a,x[i+8],20,1163531501); a = gg(a,b,c,d,x[i+13],5,-1444681467); d = gg(d,a,b,c,x[i+2],9,-51403784); c = gg(c,d,a,b,x[i+7],14,1735328473); b = gg(b,c,d,a,x[i+12],20,-1926607734); a = hh(a,b,c,d,x[i+5],4,-378558); d = hh(d,a,b,c,x[i+8],11,-2022574463); c = hh(c,d,a,b,x[i+11],16,1839030562); b = hh(b,c,d,a,x[i+14],23,-35309556); a = hh(a,b,c,d,x[i+1],4,-1530992060); d = hh(d,a,b,c,x[i+4],11,1272893353); c = hh(c,d,a,b,x[i+7],16,-155497632); b = hh(b,c,d,a,x[i+10],23,-1094730640); a = hh(a,b,c,d,x[i+13],4,681279174); d = hh(d,a,b,c,x[i],11,-358537222); c = hh(c,d,a,b,x[i+3],16,-722521979); b = hh(b,c,d,a,x[i+6],23,76029189); a = hh(a,b,c,d,x[i+9],4,-640364487); d = hh(d,a,b,c,x[i+12],11,-421815835); c = hh(c,d,a,b,x[i+15],16,530742520); b = hh(b,c,d,a,x[i+2],23,-995338651); a = ii(a,b,c,d,x[i],6,-198630844); d = ii(d,a,b,c,x[i+7],10,1126891415); c = ii(c,d,a,b,x[i+14],15,-1416354905); b = ii(b,c,d,a,x[i+5],21,-57434055); a = ii(a,b,c,d,x[i+12],6,1700485571); d = ii(d,a,b,c,x[i+3],10,-1894986606); c = ii(c,d,a,b,x[i+10],15,-1051523); b = ii(b,c,d,a,x[i+1],21,-2054922799); a = ii(a,b,c,d,x[i+8],6,1873313359); d = ii(d,a,b,c,x[i+15],10,-30611744); c = ii(c,d,a,b,x[i+6],15,-1560198380); b = ii(b,c,d,a,x[i+13],21,1309151649); a = ii(a,b,c,d,x[i+4],6,-145523070); d = ii(d,a,b,c,x[i+11],10,-1120210379); c = ii(c,d,a,b,x[i+2],15,718787259); b = ii(b,c,d,a,x[i+9],21,-343485551); a = add(a, olda); b = add(b, oldb); c = add(c, oldc); d = add(d, oldd); } return rhex(a) + rhex(b) + rhex(c) + rhex(d); }; })(); (async function () { if (location.hostname.indexOf("vdn.apps.cntv.cn") == -1) { setTimeout(()=>{ if (window.flashPlayerList?.length > 0){ window.flashPlayerList?.forEach((i,n)=>{ i = i.substring(12); let newguid = window.vodPlayerObjs[i]?.videoCenterId; console.log(newguid) const params = { pid: newguid, client: 'flash', im: '0', tsp: Math.floor(Date.now() / 1000).toString(), vn: '2049', vc: null, uid: '826D8646DEBBFD97A82D23CAE45A55BE', wlan: '' }; params.vc = md5(params.tsp + params.vn + "47899B86370B879139C08EA3B5E88267" + params.uid); const sps = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== null && value !== undefined) { sps.append(key, value); } }); const spstr = sps.toString(); let base = "https://vdn.apps.cntv.cn"; let pathname = "/api/getHttpVideoInfo.do"; let apihref = base + pathname + `?${spstr}`; let bts = n * 40 + 20; let btn = document.createElement("a"); btn.href = apihref; btn.id = "btn" + n; btn.type = "button"; btn.target = "_blank"; btn.textContent = "点击跳转到下载页" + (n>0 ? n+1 : ""); btn.style = ` position: fixed; z-index: 999; bottom: ${bts}px; right: 20px; background-color: #f86336; color: white; padding: 5px; border: none; cursor: pointer; font-size: 16px; `; document.body.appendChild(btn); }) } else if (window.loading_video){ let newguid = window.loading_video.toString().match(/centerid.*"([0-9a-f]{6,32})"/i)?.[1]; let base = "https://vdn.apps.cntv.cn"; let pathname = "/api/getHttpVideoInfo.do"; let apihref = base + pathname + `?client=flash&im=0&pid=${newguid}`; let bts = 20; let btn = document.createElement("a"); btn.href = apihref; btn.id = "btn"; btn.type = "button"; btn.target = "_blank"; btn.textContent = "点击跳转到下载页"; btn.style = ` position: fixed; z-index: 999; bottom: ${bts}px; right: 20px; background-color: #f86336; color: white; padding: 5px; border: none; cursor: pointer; font-size: 16px; `; document.body.appendChild(btn); } },1500); } if (location.hostname.indexOf("vdn.apps.cntv.cn") > -1) { const prebody = document?.body?.querySelector('pre')?.textContent || document?.body?.textContent; const data = JSON.parse(prebody); let hlsUrl = data?.hls_url; let title = data?.title?.replaceAll(" ",""); let totalLengthSec = Number(data?.video?.totalLength || 0); let brt = [450,850,1200,2000,4000]; let brtnum = data?.video?.validChapterNum; let bi = (brtnum > 0 && brtnum < 3) ? brtnum -1 : 1; // 先获取包含main的原始m3u8文件 const mainResponse = await fetch(data?.hls_url); if (mainResponse.ok && mainResponse.status == 200){ const m3u8Content = await mainResponse.text(); // 如果是4K频道,优先使用4000 if (data?.play_channel?.indexOf("4K") > 0) { bi = 4; } else if(m3u8Content.includes("1200.m3u8")){ bi = 3; } hlsUrl = data?.hls_url?.replaceAll("main", brt[bi]); } // 验证最终选择的hlsUrl是否可用 const finalResponse = await fetch(hlsUrl); if (!finalResponse.ok) { document.body.innerHTML = "版权原因无法获取"; throw new Error("遇到问题,退出"); } else { console.info(hlsUrl); } const url = new URL(hlsUrl); const filename = url.pathname.split('/').pop(); console.log(filename); document.body.innerHTML="

"; if (brtnum > 3){ let h5etag =document.createElement("p"); const h5e = data?.manifest?.hls_h5e_url.replaceAll("main","2000"); let cdn = h5e.split("/")[2]; let thisguid = h5e.split("/")[10]; h5etag.id = "h5etag"; h5etag.textContent = cdn +"<->"+thisguid; h5etag.innerHTML += "
".concat(h5e.link(h5e)); h5etag.style = ` padding: 2px; border: none; font-size: 16px;`; document.querySelector("#ht").appendChild(h5etag); } let hlstag = document.createElement("a"); hlstag.href = hlsUrl; hlstag.alt = hlsUrl; hlstag.id = "hlstag"; hlstag.target = "_blank"; hlstag.textContent = hlsUrl; hlstag.style = ` padding: 2px; border: none; cursor: pointer; font-size: 16px;`; document.querySelector("#ht").appendChild(hlstag); let ttt = document.createElement("p"); ttt.id = "vtitle"; ttt.target = "_blank"; ttt.textContent = title; ttt.style = ` padding: 5px; border: none; font-size: 16px;`; document.body.appendChild(ttt); if (confirm("是否开始下载?\r\n"+filename)){ await dmv(hlsUrl, totalLengthSec, title + '.ts', { onProgress: (current, total) => { let cotp = `${Math.round((current / total) * 100)}`; ttt.textContent = title + "---下载进程" + cotp + "%"; console.info(`Progress: ${current}/${total} (${cotp}%)`); } }); } } async function dmv(m3u8Url, totalLengthSec, outputFilename = 'video.ts', options = {}) { try { // 1. 将 URL 模板最后一段(2000.m3u8) 替换为 tsindex.ts let urlObj = new URL(m3u8Url); urlObj.search = ''; let pathArr = urlObj.pathname.split('/'); pathArr[pathArr.length - 1] = 'tsindex.ts'; urlObj.pathname = pathArr.join('/'); let baseUrl = urlObj.toString(); // 2. 根据总时长计算理论最大索引 (按 10秒/片 向上取整减 1) const maxIndex = Math.max(0, Math.ceil(totalLengthSec / 10) - 1); const estimatedTotal = maxIndex + 1; console.log(`预估总时长: ${totalLengthSec}秒, 预估切片总数:${estimatedTotal}, 最大索引: ${maxIndex}`); let segments = []; // 解析TS分片URL for (let i = 0; i < estimatedTotal; i++) { let segmentUrl = baseUrl.replace("tsindex", i); segments.push(segmentUrl); } if (segments.length === 0 || baseUrl == segments[0] ) { throw new Error('标志位替换失败'); } console.log(`找到 ${segments.length} 个 TS切片`); // 2. 下载所有分片 console.log('下载切片...'); const blobs = []; const { onProgress } = options; for (let i = 0; i < segments.length; i++) { try { const segmentResponse = await fetch(segments[i]); if (!segmentResponse.ok) throw new Error(`Failed to fetch segment: ${segmentResponse.status}`); const blob = await segmentResponse.blob(); blobs.push(blob); // 调用进度回调 if (typeof onProgress === 'function') { onProgress(i + 1, segments.length); } } catch (error) { console.error(`Error downloading segment ${segments[i]}:`, error); throw error; // 可以选择继续或抛出错误 } } // 3. 合并并下载 console.log('合并且下载...'); const mergedBlob = new Blob(blobs, { type: 'video/mp2t' }); const url = URL.createObjectURL(mergedBlob); const a = document.createElement('a'); a.href = url; a.download = outputFilename; document.body.appendChild(a); a.click(); // 清理 setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100); console.log('下载完成!'); return true; } catch (error) { console.error('合并下载过程出错:', error); throw error; } } })(); // const guid = window.guid || (window.loading_video && loading_video.toString().match(/centerid.*"([0-9a-f]{6,32})"/i)?.[1]);