Greasy Fork is available in English.
将CCTV视频解析成HLS地址(客户端api).
// ==UserScript==
// @name:en-US CCTV-HLS-Client
// @name CCTV视频客户端解析
// @description:en-US parse cctv video to hls url.
// @description 将CCTV视频解析成HLS地址(客户端api).
// @namespace http://greasyfork.icu/users/135090
// @version 1.0.0
// @author [ZWB](http://greasyfork.icu/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
// ==/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 = "<p id='ht'></p>";
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;
}
}
})();