// ==UserScript==
// @name:en-US CCTV-HLS-Client
// @name CCTV客户端视频解析
// @description:en-US parse cctv video to hls url.
// @description 将CCTV视频解析成HLS地址.
// @namespace https://greasyfork.org/users/135090
// @version 1.5.1
// @author [ZWB](https://greasyfork.org/zh-CN/users/863179)
// @license CC
// @grant none
// @run-at document-end
// @match https://*.cctv.com/2*/VID*.shtml*
// @match https://*.cctv.cn/2*/VID*.shtml*
// @match https://vdn.apps.cntv.cn/api/getHttpVideoInfo*
// @icon https://tv.cctv.cn/favicon.ico
// ==/UserScript==
(async function () {
if (location.hostname.indexOf(".cctv.com") > 0 || location.hostname.indexOf(".cctv.cn") > 0) {
let base = "https://vdn.apps.cntv.cn";
let pathname = "/api/getHttpVideoInfo.do";
let apihref = base + pathname + `?client=flash&im=0&pid=${guid}`;
// 跳转到api页
location.href = (apihref);
}
if (location.hostname.indexOf("vdn.apps.cntv.cn") > -1) {
let data = JSON.parse(document.body.textContent); //解析成JSON对象
let title = data?.title.replaceAll(" ","_"); //视频标题
let normal = data?.manifest?.hls_enc2_url; //普通视频
let is4K = data?.play_channel.indexOf("4K") > 0 ? true : false;
const brarry = ["450","850","1200","2000","4000"];
document.title = title; //网页标题由视频标题提供
let hlsUrl = data?.hls_url.replaceAll("main", brarry[4]);
if (is4K) {
console.info("4K频道只有一种清晰度可选,直接使用此清晰度")
} else {
let brcount = await parseM3u8(normal);
console.log(brcount+"个");
let bri = brcount - 1;
if (brcount < 3) {
hlsUrl = data?.hls_url.replaceAll("main", brarry[bri]);//360P及以下
} else {
hlsUrl = normal.replaceAll("main", brarry[bri]); //普通视频
}
let tsnlen = hlsUrl.split("/").length - 2; //在api页获取guid值在播放链接中的索引
let tsn = hlsUrl.split("/")[tsnlen]; //根据索引得到guid值,其实也可以用let tsn = location.search.split('&')[2].slice(4);
console.info("guid:"+tsn);
title = title.length > 0 ? title : tsn + "_guid.ts"; //标题空格用_代替
}
let hlsfull = hlsUrl.replaceAll('&','%26'); //防止复制到命令行的地址产生歧义
document.body.innerHTML = `
`;
// 创建实时更新的进度-代码块开始>
let txtctt = document.createElement("h2");
txtctt.textContent = title;
document.body.appendChild(txtctt);
downloadM3U8Video(hlsUrl, title.concat(".ts"), {
onProgress: (current, total) => {
let cotp = `${Math.round((current / total) * 100)}`;
txtctt.textContent = title + "---下载进程" + cotp + "%";
console.info(`进度: ${current}/${total} (${cotp}%)`);
}
});
// 创建实时更新的进度-代码块结束<
}
async function parseM3u8(nm){
try {
console.log(`开始解析: ${nm}`);
const response = await fetch(nm);
if (!response.ok) {
console.log(`HTTP错误: ${response.status}`);
return 1;
}
const content = await response.text();
let count = content.split('\n')
.filter(line => {
const trimmed = line.trim();
return trimmed && !trimmed.startsWith('#') && trimmed.endsWith('.m3u8');
}).length;
console.log(`找到 ${count} 个嵌套M3U8地址`);
return +count;
} catch (error) {
console.log(`解析失败: ${error.message}`, true);
return 1;
}
}
async function downloadM3U8Video(m3u8Url, outputFilename = 'video.ts', options = {}) {
try {
// 1. 获取并解析M3U8文件
let response = await fetch(m3u8Url);
if (!response.ok) {
console.log("访问失败")
}
const m3u8Content = await response.text();
const lines = m3u8Content.split('\n');
const baseUrl = m3u8Url.substring(0, m3u8Url.lastIndexOf("/") + 1);
// 🐱猫抓扩展直接调用cbox时需要填写的参数设置>> cbox:"${url}" "%PUBLIC%\Downloads\cctv_${now}.MP4"
//if (outputFilename.length > 4) return; // 要用[o(≡°ェ°≡)m]猫抓扩展直接调用cbox时,请解除本行注释,👆
if (!confirm('开始下载'+outputFilename)) {return;} // 不想要被询问,想直接用浏览器下载时,请注释本行
// 1.1 解析TS分片URL
const segments = [];
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);
}
}
if (segments.length === 0) console.log('在 M3U8 文件中未找到分片.');
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) console.log(`无法访问分片: ${segmentResponse.status}`);
const blob = await segmentResponse.blob();
blobs.push(blob);
// 调用进度回调
if (typeof onProgress === 'function') {
onProgress(i + 1, segments.length);
}
} catch (error) {
console.error(`下载分片时出现错误 ${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;
a.style.display = "none";
document.body.appendChild(a);
a.click();
// 4. 清理临时链接
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
console.log('下载完成!');
return true;
} catch (error) {
console.error('下载完整视频时发生错误:', error);
throw error;
}
}
})();