// ==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.6 // @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 = (() => { const rot = (x, n) => (x << n) | (x >>> (32 - n)); const hex = (n) => { let h = ''; for (let i = 7; i >= 0; i--) h += ((n >>> (i * 4)) & 0xF).toString(16); return h; }; return (s) => { const utf8 = []; for (let i = 0; i < s.length; i++) { let c = s.charCodeAt(i); if (c < 128) utf8.push(c); else if (c < 2048) utf8.push((c >> 6) | 192, (c & 63) | 128); else utf8.push((c >> 12) | 224, ((c >> 6) & 63) | 128, (c & 63) | 128); } const len = utf8.length * 8; utf8.push(0x80); while ((utf8.length * 8) % 512 !== 448) utf8.push(0); for (let i = 0; i < 8; i++) utf8.push((len >>> (i * 8)) & 0xFF); let h0 = 0x67452301, h1 = 0xEFCDAB89, h2 = 0x98BADCFE, h3 = 0x10325476; for (let i = 0; i < utf8.length; i += 64) { let w = []; for (let j = 0; j < 16; j++) { w[j] = utf8[i + j*4] | (utf8[i + j*4 + 1] << 8) | (utf8[i + j*4 + 2] << 16) | (utf8[i + j*4 + 3] << 24); } for (let j = 16; j < 80; j++) { w[j] = rot(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1); } let a = h0, b = h1, c = h2, d = h3; for (let j = 0; j < 80; j++) { let f, k; if (j < 20) { f = (b & c) | (~b & d); k = 0x5A827999; } else if (j < 40) { f = b ^ c ^ d; k = 0x6ED9EBA1; } else if (j < 60) { f = (b & c) | (b & d) | (c & d); k = 0x8F1BBCDC; } else { f = b ^ c ^ d; k = 0xCA62C1D6; } let temp = rot(a, 5) + f + d + k + w[j]; d = c; c = b; b = rot(b, 30); a = temp; } h0 = (h0 + a) | 0; h1 = (h1 + b) | 0; h2 = (h2 + c) | 0; h3 = (h3 + d) | 0; } return hex(h0) + hex(h1) + hex(h2) + hex(h3); }; })(); (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 data = await JSON.parse(document?.body?.textContent); let hlsUrl = data?.hls_url; let title = data?.title?.replaceAll(" ",""); 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 downloadM3U8Video(hlsUrl, title + '.ts', { onProgress: (current, total) => { let cotp = `${Math.round((current / total) * 100)}`; ttt.textContent = title + "---下载进程" + cotp + "%"; console.info(`Progress: ${current}/${total} (${cotp}%)`); } }); } } async function downloadM3U8Video(m3u8Url, outputFilename = 'video.m2t', options = {}) { try { // 1. 获取并解析M3U8文件 const response = await fetch(m3u8Url); if (!response.ok) throw new Error(`Failed to fetch M3U8: ${response.status}`); const m3u8Content = await response.text(); const lines = m3u8Content.split('\n'); const baseUrl = m3u8Url.substring(0, m3u8Url.lastIndexOf("/") + 1); const segments = []; // 解析TS分片URL for (const line of lines) { if (line && !line.startsWith('#') && (line.endsWith('.ts') || line.match(/\.ts\?/))) { const segmentUrl = line.startsWith('http') ? line : new URL(line, baseUrl).href; segments.push(segmentUrl); // return; } } if (segments.length === 0) throw new Error('No TS segments found in the M3U8 file'); console.log(`Found ${segments.length} TS segments`); // 2. 下载所有分片 console.log('Downloading segments...'); 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('Merging and downloading...'); 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('Download completed!'); return true; } catch (error) { console.error('Error downloading M3U8 video:', error); throw error; } } })(); // const guid = window.guid || (window.loading_video && loading_video.toString().match(/centerid.*"([0-9a-f]{6,32})"/i)?.[1]);