Bilibili 动态视频提取器

在Bilibili动态页(t.bilibili.com)右侧创建一个可收起的工具面板,使用图标按钮扫描并提取视频的标题和链接,支持按“标题链接”格式批量复制。

// ==UserScript==
// @name         Bilibili 动态视频提取器
// @namespace    http://tampermonkey.net/
// @version      2.5
// @description  在Bilibili动态页(t.bilibili.com)右侧创建一个可收起的工具面板,使用图标按钮扫描并提取视频的标题和链接,支持按“标题链接”格式批量复制。
// @author       Gemini Code Assist
// @match        https://t.bilibili.com/*
// @icon         https://www.bilibili.com/favicon.ico
// @grant        GM_addStyle
// @grant        GM_setClipboard
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- Icon Definitions ---
    const ICONS = {
        scan: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>`,
        copy: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`,
        check: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>`
    };

    let extractedVideos = []; // 用于存储提取的视频信息

    /**
     * 扫描当前页面的视频并更新UI
     */
    function scanAndDisplayVideos() {
        const resultsContainer = document.getElementById('bili-feed-extractor-results');
        const statusElement = document.getElementById('bili-feed-extractor-status');
        const copyButton = document.getElementById('bili-feed-copy-button');

        if (!resultsContainer || !statusElement) return;

        // 清空上一次的结果
        resultsContainer.innerHTML = '';
        extractedVideos = [];
        const seenLinks = new Set(); // 用于链接去重

        // B站动态页视频卡的CSS选择器,改为选择包含链接的<a>标签
        const videoCards = document.querySelectorAll('a.bili-dyn-card-video');

        videoCards.forEach(card => {
            // 'card' is now the <a> element
            const titleElement = card.querySelector('.bili-dyn-card-video__title');

            // 如果没有找到标题元素,则跳过
            if (!titleElement) {
                return;
            }

            const title = titleElement.innerText.trim();
            let link = card.href;

            // 健壮性检查。如果获取不到有效的链接,则跳过此卡片。
            if (typeof link !== 'string' || !link) {
                return; // 相当于 forEach 循环中的 'continue'
            }

            // 清理链接,去掉跟踪参数
            link = link.split('?')[0];

            // 去重
            if (title && !seenLinks.has(link)) {
                seenLinks.add(link); // Bug fix: was adding object instead of link string
                extractedVideos.push({ title, link });

                // 根据您的要求,此处不再将视频信息添加到面板的列表中。
                // 数据仍然会收集,以便“一键复制”功能可以正常工作。
            }
        });

        // 更新状态
        if (extractedVideos.length > 0) {
            statusElement.innerText = `扫描到 ${extractedVideos.length} 个视频。`;
            copyButton.disabled = false;
            copyButton.style.opacity = '1';
        } else {
            statusElement.innerText = '未扫描到视频。请尝试向下滚动页面再试。';
            copyButton.disabled = true;
            copyButton.style.opacity = '0.5';
        }
    }

    /**
     * 复制所有结果到剪贴板
     */
    function copyAllResults() {
        if (extractedVideos.length === 0) return;

        // 按照 "标题链接" 格式拼接,每个视频占一行
        const textToCopy = extractedVideos.map(v => `${v.title} ${v.link}`).join('\n');
        GM_setClipboard(textToCopy, 'text');

        const copyButton = document.getElementById('bili-feed-copy-button');
        copyButton.innerHTML = ICONS.check;
        copyButton.title = '已复制!';
        copyButton.disabled = true; // 暂时禁用以防重复点击

        setTimeout(() => {
            copyButton.innerHTML = ICONS.copy;
            copyButton.title = '一键复制所有信息';
            copyButton.disabled = false;
        }, 2000);
    }

    /**
     * 创建并注入UI面板
     */
    function createPanel() {
        const container = document.createElement('div');
        container.id = 'bili-feed-extractor-container';
        container.innerHTML = `
            <div id="bili-feed-extractor-header">
                <button id="bili-feed-toggle-button" title="收起/展开面板">收起</button>
            </div>
            <div id="bili-feed-extractor-body">
                <div class="button-group">
                    <button id="bili-feed-scan-button" title="扫描视频">${ICONS.scan}</button>
                    <button id="bili-feed-copy-button" title="一键复制所有信息" disabled>${ICONS.copy}</button>
                </div>
                <p id="bili-feed-extractor-status">尚未扫描</p>
                <hr>
                <div id="bili-feed-extractor-results"></div>
            </div>
        `;

        document.body.appendChild(container);
        addStyles(); // 添加样式

        // 绑定事件
        document.getElementById('bili-feed-scan-button').addEventListener('click', scanAndDisplayVideos);
        document.getElementById('bili-feed-copy-button').addEventListener('click', copyAllResults);

        const toggleButton = document.getElementById('bili-feed-toggle-button');
        const body = document.getElementById('bili-feed-extractor-body');
        toggleButton.addEventListener('click', () => {
            const isCollapsed = body.style.display === 'none';
            if (isCollapsed) {
                body.style.display = 'block';
                toggleButton.innerText = '收起';
            } else {
                body.style.display = 'none';
                toggleButton.innerText = '展开';
            }
        });


        // 初始时禁用复制按钮
        const copyButton = document.getElementById('bili-feed-copy-button');
        copyButton.style.opacity = '0.5';
    }

    /**
     * 添加UI样式
     */
    function addStyles() {
        GM_addStyle(`
            #bili-feed-extractor-container {
                position: fixed;
                top: 100px;
                right: 20px;
                width: 50px;
                max-height: 70vh;
                background-color: #ffffff;
                border: 1px solid #e3e5e7;
                border-radius: 8px;
                padding: 16px;
                z-index: 9999;
                font-size: 14px;
                color: #18191c;
                box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
                display: flex;
                flex-direction: column;
                transition: max-height 0.3s ease-in-out;
            }
            #bili-feed-extractor-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                margin-bottom: 12px;
            }
            #bili-feed-extractor-container h3 {
                margin: 0;
                font-size: 16px;
                color: #fb7299;
            }
            #bili-feed-toggle-button {
                background: none;
                border: 1px solid #e3e5e7;
                color: #61666d;
                padding: 2px 8px;
                font-size: 12px;
                border-radius: 4px;
                cursor: pointer;
            }
            #bili-feed-toggle-button:hover {
                background-color: #f1f2f3;
            }
            #bili-feed-extractor-container .button-group {
                display: flex;
                gap: 10px;
                margin-bottom: 10px;
            }
            #bili-feed-extractor-container .button-group button {
                width: 42px;
                height: 34px;
                display: flex;
                align-items: center;
                justify-content: center;
                background-color: #00a1d6;
                color: white;
                border: none;
                padding: 0;
                border-radius: 5px;
                cursor: pointer;
                transition: background-color 0.2s, opacity 0.2s;
            }
            #bili-feed-extractor-container button:hover:not(:disabled) {
                background-color: #00b5e5;
            }
            #bili-feed-extractor-container button:disabled {
                cursor: not-allowed;
            }
            #bili-feed-extractor-container #bili-feed-extractor-status {
                text-align: center;
                color: #61666d;
                font-size: 12px;
                margin: 0;
            }
            #bili-feed-extractor-container hr {
                border: none;
                border-top: 1px solid #e3e5e7;
                margin: 12px 0;
            }
            #bili-feed-extractor-results {
                overflow-y: auto;
                flex-grow: 1;
            }
            #bili-feed-extractor-results .video-item {
                border-bottom: 1px solid #f1f2f3;
                padding-bottom: 10px;
                margin-bottom: 10px;
            }
            #bili-feed-extractor-results .video-item:last-child {
                border-bottom: none;
                margin-bottom: 0;
            }
            #bili-feed-extractor-results p {
                margin: 0 0 5px 0;
                word-wrap: break-word;
                line-height: 1.5;
            }
            #bili-feed-extractor-results strong {
                color: #61666d;
            }
            #bili-feed-extractor-results a {
                color: #00a1d6;
                text-decoration: none;
            }
            #bili-feed-extractor-results a:hover {
                text-decoration: underline;
            }
        `);
    }

    // 页面加载后直接创建面板
    createPanel();

})();