Greasy Fork

来自缓存

Greasy Fork is available in English.

CCTV视频客户端解析

将CCTV视频解析成HLS地址(客户端api).

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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;
        }
    }
})();