// ==UserScript== // @name:en-US CCTV-HLS-Client // @name CCTV视频客户端解析 // @description:en-US parse cctv video to hls url. // @description 将CCTV视频解析成HLS地址(客户端api). // @namespace https://greasyfork.org/users/135090 // @version 1.0.0 // @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 *://vdn.apps.cntv.cn/api/getHttpVideoInfo* // @icon https://tv.cctv.cn/favicon.ico // @downloadURL https://update.greasyfork.icu/scripts/558396/CCTV%E8%A7%86%E9%A2%91%E5%AE%A2%E6%88%B7%E7%AB%AF%E8%A7%A3%E6%9E%90.user.js // @updateURL https://update.greasyfork.icu/scripts/558396/CCTV%E8%A7%86%E9%A2%91%E5%AE%A2%E6%88%B7%E7%AB%AF%E8%A7%A3%E6%9E%90.meta.js // ==/UserScript== (async function () { if (location.hostname.indexOf("vdn.apps.cntv.cn") == -1) { setTimeout(() => { let vppl = window?.vodh5player?.playerList; if (vppl?.length > 1) { vppl?.forEach((i, n) => { let newguid = i?.options_?.paras?.videoId; let base = "https://vdn.apps.cntv.cn"; let pathname = "/api/getHttpVideoInfo.do"; let apihref = base + pathname + `?client=flash&im=0&pid=${newguid}`; 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 + 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); }); throw new Error("多个"); } else { let centerid = window?.vodPlayerObjs?._video?.videoCenterId || window?.guid; let videoid = window?.vodh5player?.playerList[0]?.options_?.sources[0]?.src?.split("/")[10]; let newguid = (centerid?.length == 32) ? centerid : videoid; let base = "https://vdn.apps.cntv.cn"; let pathname = "/api/getHttpVideoInfo.do"; let apihref = base + pathname + `?client=flash&im=0&pid=${newguid}`; 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: 20px; right: 20px; background-color: #f86336; color: white; padding: 5px; border: none; cursor: pointer; font-size: 16px; `; document.body.appendChild(btn); throw new Error("单个"); } }, 1500); } if (location.hostname.indexOf("vdn.apps.cntv.cn") > -1) { let data = await JSON.parse(document?.body?.textContent); let enc2url = new URL(data?.manifest?.hls_enc2_url); enc2url.hostname = "dh5cntv.a.bdydns.com"; // 另一个CDN域名http://dh5.cntv.baishancdnx.cn let hls2Url = enc2url.toString(); let orgtitle = data?.title; /* \u005c → \ (反斜杠) \u002f → / (正斜杠) \u003a → : (冒号) \u002a → * (星号) \u003f → ? (问号) \u003c → < (小于号) \u003e → > (大于号) \u007c → | (竖线) \u0020 → (半角空格) \u0022 → " (双引号) \u0027 → ' (单引号) \u3000 → (全角空格) \uff02 → " (全角双引号) \uff07 → ' (全角单引号) 将所有文件名非法字符和容易被识别成半角的全角字符都替换成_ */ const clean = s => s.replace(/[\u005c\u002f\u003a\u002a\u003f\u003c\u003e\u007c\u0020\u0022\u0027\u3000\uff02\uff07]/g, '_'); const title = clean(orgtitle); // 先获取包含main的原始m3u8文件 let hlsUrl = data?.hls_url; const mainResponse = await fetch(hlsUrl); if (!mainResponse.ok) { document.body.innerHTML = "无法获取主m3u8文件"; throw new Error("无法获取主m3u8文件"); } var brt = [450,850,1200,2000,4000]; let brti = 0; if (data?.video?.validChapterNum > 1){ brti = data?.video?.validChapterNum - 1; } // 如果是4K频道,优先使用4000 if (!mainResponse.ok) { document.body.innerHTML = "无法获取主m3u8文件"; throw new Error("无法获取主m3u8文件"); } else { const m3u8Content = await mainResponse.text(); // 如果是4K频道,优先使用4000 if (data?.play_channel?.indexOf("4K") > 0) { brti = 4; hlsUrl = data?.hls_url?.replaceAll("main", brt[brti]); } else if(m3u8Content.includes("1200.m3u8") || brti == 0 || brti == 1){ hlsUrl = data?.hls_url?.replaceAll("main", brt[brti]); } else if(!m3u8Content.includes("1200.m3u8") && brti == 2){ hlsUrl = data?.hls_url?.replaceAll("main", brt[1]); } else if(!m3u8Content.includes("1200.m3u8") && brti > 2){ hlsUrl = hls2Url?.replaceAll("main", brt[brti]); } } // 验证最终选择的hlsUrl是否可用 let finalResponse = await fetch(hlsUrl); if (!finalResponse.ok) { document.body.innerHTML = "无法获取,版权受限"; finalResponse = null; throw new Error("版权受限"); } console.info(hlsUrl); const url = new URL(hlsUrl); const filename = url.pathname.split('/').pop(); console.log(filename); document.body.innerHTML = "
"; 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) => { var 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; } } })();