抖音直播流提取

提取抖音直播地址

当前为 2024-11-06 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name                Extract Douyin Live Stream URLs
// @name:zh-CN          抖音直播流提取
// @namespace           Cassandre
// @version             1.1
// @description         Extract stream URLs from Douyin live streams
// @description:zh-CN   提取抖音直播地址
// @author              Cassandre Cora
// @license MIT
// @icon                https://p3-pc-weboff.byteimg.com/tos-cn-i-9r5gewecjs/logo-horizontal-small.svg
// @match               https://live.douyin.com/*
// @run-at              document-end
// @grant               GM_addStyle
// @grant               GM_setClipboard
// ==/UserScript==

const QUALITY_MAP = {
    'FULL_HD1': '原画',
    'HD1': '超清',
    'SD2': '高清',
    'SD1': '标清'
};

const QUALITY_LEVELS = ['FULL_HD1', 'HD1', 'SD2', 'SD1'];

const STYLES = `
        .douyin-stream-url-side-button {
            position: fixed;
            z-index: 19998;
            top: 50%;
            right: 0;
            width: 40px;
            height: 40px;
            border: none;
            outline: none;
            cursor: pointer;
            color: white;
            text-align: center;
            background: linear-gradient(135deg, #FE2C55 0%, #FF4B75 100%);
            border-radius: 50%;
            transition: all 0.3s ease;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .douyin-stream-url-side-button:hover {
            transform: scale(1.1);
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
            background: linear-gradient(135deg, #FF4B75 0%, #FE2C55 100%);
            color: #f0f0f0;
            font-weight: bold;
        }
        .douyin-stream-url-side-button::before {
            content: "⚡";
            font-size: 20px;
        }
        .douyin-stream-url-close-button {
            position: absolute;
            top: -8px;
            right: -8px;
            width: 24px;
            height: 24px;
            border: 2px solid #FE2C55;
            border-radius: 50%;
            background: white;
            cursor: pointer;
            outline: none;
            font-size: 14px;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: all 0.3s ease;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }
        .douyin-stream-url-close-button:hover {
            transform: scale(1.1);
            background: #FE2C55;
            color: white;
            font-weight: bold;
        }
        #douyin-stream-url-app {
            position: fixed;
            right: 20px;
            top: 110px;
            width: 320px;
            height: auto;
            opacity: 0;
            background-color: rgba(24, 24, 24, 0.95);
            color: #e0e0e0;
            padding: 15px;
            font-size: 13px;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            z-index: 9999;
            border-radius: 16px;
            transform: translateX(110%);
            transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
            box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
            backdrop-filter: blur(10px);
        }
        .douyin-stream-url-list-container {
            background-color: rgba(31, 31, 31, 0.8);
            border-radius: 12px;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
            margin-bottom: 12px;
            overflow: hidden;
        }
        .douyin-stream-url-list-container:last-child {
            margin-bottom: 0;
        }
        .douyin-stream-url-list-header {
            background-color: rgba(45, 45, 45, 0.8);
            padding: 10px 12px;
            font-weight: 600;
            font-size: 14px;
            color: #ffffff;
            border-bottom: 1px solid rgba(255, 255, 255, 0.1);
        }
        .douyin-hls-stream-url-list-content, .douyin-flv-stream-url-list-content {
            padding: 8px;
            overflow-x: auto;
            white-space: nowrap;
            background-color: rgba(38, 38, 38, 0.8);
            font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
            font-size: 13px;
            line-height: 1.6;
            color: #d1d1d1;
            max-height: 150px;
            overflow-y: auto;
            border-bottom: 1px solid rgba(255, 255, 255, 0.1);
        }
        .douyin-stream-url-button-container {
            display: flex;
            gap: 4px;
            padding: 4px;
        }
        .douyin-hls-stream-url-copy-button, .douyin-flv-stream-url-copy-button {
            display: flex;
            align-items: center;
            justify-content: center;
            width: 50%;
            padding: 10px;
            background: linear-gradient(135deg, #FE2C55 0%, #FF4B75 100%);
            color: white;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            transition: all 0.3s ease;
            font-size: 14px;
            font-weight: 500;
        }
        .douyin-hls-stream-url-copy-button:hover, .douyin-flv-stream-url-copy-button:hover {
            transform: translateY(-1px);
            box-shadow: 0 4px 12px rgba(254, 44, 85, 0.3);
            background: linear-gradient(135deg, #FF4B75 0%, #FE2C55 100%);
            color: #ffffff;
            font-weight: 600;
            letter-spacing: 0.5px;
        }
        .douyin-stream-url-download-all-button {
            display: flex;
            align-items: center;
            justify-content: center;
            width: 100%;
            padding: 12px;
            background: linear-gradient(135deg, #FE2C55 0%, #FF4B75 100%);
            color: white;
            border: none;
            cursor: pointer;
            transition: all 0.3s ease;
            font-size: 14px;
            font-weight: 500;
            border-radius: 10px;
            margin-top: 8px;
        }
        .douyin-stream-url-download-all-button:hover {
            transform: translateY(-1px);
            box-shadow: 0 4px 12px rgba(254, 44, 85, 0.3);
            background: linear-gradient(135deg, #FF4B75 0%, #FE2C55 100%);
            color: #ffffff;
            font-weight: 600;
            letter-spacing: 0.5px;
        }
    `;

// Get stream ID from URL
let streamId = window.location.href.split('/root/live/')[1]?.split('?')[0];

// Inject styles
GM_addStyle(STYLES);

class DouyinStreamExtractor {
    constructor() {
        this.pageHTML = document.documentElement.outerHTML;
        this.init();
    }

    init() {
        const streamData = this.getStreamData();
        if (streamData?.status) {
            this.createUI(streamData);
        }
    }

    onUrlChange(callback) {
        // Listen for popstate events
        window.addEventListener('popstate', () => callback({
            type: 'popstate',
            url: window.location.href
        }));

        // Override pushState and replaceState
        const originalPushState = history.pushState;
        history.pushState = function (...args) {
            originalPushState.apply(this, args);
            callback({
                type: 'pushState',
                url: window.location.href
            });
        };

        const originalReplaceState = history.replaceState;
        history.replaceState = function (...args) {
            originalReplaceState.apply(this, args);
            callback({
                type: 'replaceState',
                url: window.location.href
            });
        };

        // Listen for hash changes
        window.addEventListener('hashchange', () => callback({
            type: 'hashchange',
            url: window.location.href
        }));
    }

    extractJSON(pattern) {
        const match = this.pageHTML?.match(pattern);
        return match ? match[1].replace(/\\/g, '').replace(/u0026/g, '&') : null;
    }

    getStreamData() {
        try {
            const jsonStr = this.extractJSON(/(\{\\"state\\":.*?)]\\n"]\)/) ||
                this.extractJSON(/(\{\\"common\\":.*?)]\\n"]\)<\/script><div hidden/);

            if (!jsonStr) {
                console.warn("Page JSON data not found");
                return null;
            }

            const roomStoreMatch = jsonStr.match(/"roomStore":(.*?),"linkmicStore"/);
            if (!roomStoreMatch) {
                console.warn("Room data not found");
                return null;
            }

            const roomStore = `${roomStoreMatch[1].split(',"has_commerce_goods"')[0]}}}}`;
            const roomData = JSON.parse(roomStore)?.roomInfo?.room;

            if (!roomData) {
                console.warn("Invalid room data structure");
                return null;
            }

            const anchorNameMatch = roomStore.match(/"nickname":"(.*?)","avatar_thumb/);

            return {
                id: roomData.id_str,
                status: roomData.status === 2,
                anchor_name: anchorNameMatch?.[1] || '',
                hls_stream_url: roomData.stream_url?.hls_pull_url_map,
                flv_stream_url: roomData.stream_url?.flv_pull_url,
                title: roomData.title,
                avatar_thumb: roomData.owner?.avatar_thumb
            };

        } catch (error) {
            console.error("Error parsing room data:", error);
            return null;
        }
    }

    createStreamUrlList(data) {
        return QUALITY_LEVELS.reduce((acc, quality) => {
            acc[quality] = {
                hls_stream_url: data.hls_stream_url?.[quality] || null,
                flv_stream_url: data.flv_stream_url?.[quality] || null
            };
            return acc;
        }, {});
    }

    async copyToClipboard(text, button, type) {
        try {
            if (GM_setClipboard) {
                await GM_setClipboard(text);
            } else {
                await navigator.clipboard.writeText(text);
            }
            button.textContent = '已复制!';
            setTimeout(() => {
                button.textContent = `复制 ${type}`;
            }, 1000);
        } catch (err) {
            console.error('复制失败:', err);
            button.textContent = '复制失败!';
        }
    }

    downloadM3U8(content, filename) {
        const blob = new Blob([content], { type: 'application/x-mpegURL' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    }

    createUI(data) {
        const app = document.createElement('div');
        app.id = 'douyin-stream-url-app';

        const urlList = this.createStreamUrlList(data);

        // Create quality list
        Object.entries(urlList).forEach(([quality, urls]) => {
            if (!urls.hls_stream_url && !urls.flv_stream_url) return;

            const container = this.createQualityContainer(quality, urls);
            app.appendChild(container);
        });

        // Create download all button
        const downloadAllButton = this.createDownloadAllButton(data, urlList);
        app.appendChild(downloadAllButton);

        // Create side button and close button
        const { sideButton, closeButton } = this.createControlButtons(app);

        document.body.appendChild(app);
        document.body.appendChild(sideButton);
        app.appendChild(closeButton);
    }

    createQualityContainer(quality, urls) {
        const container = document.createElement('div');
        container.className = 'douyin-stream-url-list-container';

        const header = document.createElement('div');
        header.className = 'douyin-stream-url-list-header';
        header.textContent = `${QUALITY_MAP[quality]} ${quality}`;
        container.appendChild(header);
        /*
        if (urls.hls_stream_url) {
            const hlsContent = document.createElement('pre');
            hlsContent.className = 'douyin-hls-stream-url-list-content';
            hlsContent.textContent = urls.hls_stream_url;
            container.appendChild(hlsContent);
        }
 
        if (urls.flv_stream_url) {
            const flvContent = document.createElement('pre');
            flvContent.className = 'douyin-flv-stream-url-list-content';
            flvContent.textContent = urls.flv_stream_url;
            container.appendChild(flvContent);
        }
        */
        const buttonContainer = this.createButtonContainer(quality, urls);
        container.appendChild(buttonContainer);

        return container;
    }

    createButtonContainer(quality, urls) {
        const container = document.createElement('div');
        container.className = 'douyin-stream-url-button-container';

        if (urls.hls_stream_url) {
            const hlsButton = document.createElement('button');
            hlsButton.className = 'douyin-hls-stream-url-copy-button';
            hlsButton.textContent = '复制 HLS';
            hlsButton.onclick = () => this.copyToClipboard(urls.hls_stream_url, hlsButton, 'HLS');
            container.appendChild(hlsButton);
        }

        if (urls.flv_stream_url) {
            const flvButton = document.createElement('button');
            flvButton.className = 'douyin-flv-stream-url-copy-button';
            flvButton.textContent = '复制 FLV';
            flvButton.onclick = () => this.copyToClipboard(urls.flv_stream_url, flvButton, 'FLV');
            container.appendChild(flvButton);
        }

        return container;
    }

    createDownloadAllButton(data, urlList) {
        const button = document.createElement('button');
        button.className = 'douyin-stream-url-download-all-button';
        button.textContent = 'M3U8文件下载';

        button.onclick = () => {
            let m3u8Content = '#EXTM3U\n';
            Object.entries(urlList).forEach(([quality, urls]) => {
                if (urls.hls_stream_url) {
                    m3u8Content += `#EXTINF:-1 tvg-name="${QUALITY_MAP[quality]} ${quality} hls" tvg-logo="${data.avatar_thumb.url_list[0]}"\n${urls.hls_stream_url}\n`;
                }
                if (urls.flv_stream_url) {
                    m3u8Content += `#EXTINF:-1 tvg-name="${QUALITY_MAP[quality]} ${quality} flv" tvg-logo="${data.avatar_thumb.url_list[0]}"\n${urls.flv_stream_url}\n`;
                }
            });

            const filename = `抖音直播_${data.anchor_name}_${new Date().toLocaleString('zh-CN', {
                year: 'numeric',
                month: '2-digit',
                day: '2-digit',
                hour: '2-digit',
                minute: '2-digit'
            }).replace(/[\/\s:]/g, '').replace(/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})/, '$1$2$3_$4:$5')}.m3u8`;

            this.downloadM3U8(m3u8Content, filename);

            button.textContent = 'M3U8文件成功生成!';
            setTimeout(() => {
                button.textContent = 'M3U8文件下载';
            }, 1000);
        };

        return button;
    }

    createControlButtons(app) {
        const sideButton = document.createElement('button');
        sideButton.className = 'douyin-stream-url-side-button';

        const closeButton = document.createElement('button');
        closeButton.className = 'douyin-stream-url-close-button';
        closeButton.innerHTML = '<span>X</span>';

        sideButton.onclick = () => {
            sideButton.style.display = 'none';
            app.style.transform = 'translateX(0)';
            app.style.opacity = '1';
        };

        closeButton.onclick = () => {
            app.style.transform = 'translateX(110%)';
            app.style.opacity = '0';
            sideButton.style.display = 'block';
        };

        return { sideButton, closeButton };
    }
}

// Initialize
new DouyinStreamExtractor();