Greasy Fork

Greasy Fork is available in English.

抖音小红书B站视频链接采集器

在抖音、小红书、B站的作者主页采集当前作者所有视频的访问链接

// ==UserScript==
// @name         抖音小红书B站视频链接采集器
// @namespace    http://tampermonkey.net/
// @version      0.4
// @description  在抖音、小红书、B站的作者主页采集当前作者所有视频的访问链接
// @author       Ksr Cursor
// @match        *://*.douyin.com/user/*
// @match        *://*.xiaohongshu.com/user/profile/*
// @match        *://space.bilibili.com/*/upload/video
// @grant        GM_setClipboard
// @run-at       document-end
// @license      GPL-3.0-or-later

// ==/UserScript==

(function() {
    'use strict';
    
    // 添加样式
    const style = document.createElement('style');
    style.textContent = `
        .ksr-collect-btn {
            padding: 8px 16px;
            background-color: #FF2B2B;
            color: white;
            font-size: 14px;
            font-weight: bold;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            transition: all 0.3s;
            margin-left: 10px;
        }
        .ksr-collect-btn:hover {
            background-color: #FF0000;
        }
        .ksr-toast {
            position: fixed;
            top: 80px;
            right: 20px;
            padding: 10px 20px;
            background-color: rgba(0, 0, 0, 0.7);
            color: white;
            border-radius: 4px;
            z-index: 1000000;
            opacity: 0;
            transition: opacity 0.3s;
            font-weight: bold;
            font-size: 16px;
        }
        .ksr-toast.show {
            opacity: 1;
        }
        .ksr-float-btn {
            position: fixed;
            top: 20px;
            right: 20px;
            padding: 10px 15px;
            background-color: #FF2B2B;
            color: white;
            font-size: 14px;
            font-weight: bold;
            border: 2px solid white;
            border-radius: 4px;
            cursor: pointer;
            z-index: 9999999;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
        }
    `;
    document.head.appendChild(style);

    // 创建提示气泡
    const toast = document.createElement('div');
    toast.className = 'ksr-toast';
    toast.textContent = '已复制';
    document.body.appendChild(toast);

    // 显示气泡提示
    function showToast() {
        toast.classList.add('show');
        setTimeout(() => {
            toast.classList.remove('show');
        }, 2000);
    }

    // 复制到剪贴板
    function copyToClipboard(text) {
        GM_setClipboard(text);
        showToast();
    }

    // 获取当前域名
    const currentHost = window.location.host;
    
    // 创建按钮
    function createCollectButton() {
        // 检查按钮是否已存在
        const existingBtn = document.querySelector('.ksr-collect-btn');
        if (existingBtn) return existingBtn;
        
        const collectBtn = document.createElement('button');
        collectBtn.className = 'ksr-collect-btn';
        collectBtn.textContent = '采集链接';
        collectBtn.title = '点击采集链接 (快捷键: Alt+1)';
        
        // 添加点击事件
        collectBtn.addEventListener('click', collectLinks);
        
        return collectBtn;
    }
    
    // 创建悬浮按钮(备用方案)
    function createFloatButton() {
        // 检查是否已存在
        if (document.querySelector('.ksr-float-btn')) return;
        
        const floatBtn = document.createElement('button');
        floatBtn.className = 'ksr-float-btn';
        floatBtn.textContent = '采集链接';
        floatBtn.title = '点击采集链接 (快捷键: Alt+1)';
        floatBtn.addEventListener('click', collectLinks);
        document.body.appendChild(floatBtn);
    }
    
    // 按平台插入按钮
    function insertButton() {
        if (currentHost.includes('douyin.com')) {
            insertDouyinButton();
        } else if (currentHost.includes('xiaohongshu.com')) {
            insertXiaohongshuButton();
        } else if (currentHost.includes('bilibili.com')) {
            insertBilibiliButton();
        }
    }
    
    // 为抖音寻找按钮容器的辅助函数
    function findDouyinButtonContainer() {
        const possibleContainers = [
            // 1. 尝试通过关注按钮查找
            Array.from(document.querySelectorAll('button')).filter(btn => 
                btn.textContent && (
                    btn.textContent.includes('关注') || 
                    btn.textContent.includes('已关注') || 
                    btn.textContent.includes('私信') ||
                    btn.textContent.includes('分享')
                )
            ),
            // 2. 尝试通过按钮容器类查找
            document.querySelectorAll('[class*="action"], [class*="button-container"], [class*="profile-header"], [class*="tool"]'),
            // 3. 尝试抖音常用UI组件
            document.querySelectorAll('[class*="user-info"], [class*="header-right"], [class*="operation"]')
        ];
        
        // 扁平化并过滤掉空结果
        const containers = possibleContainers
            .flat()
            .filter(el => el && el.parentNode);
        
        if (containers.length > 0) {
            // 优先考虑已有多个按钮的容器
            for (const container of containers) {
                const buttons = container.querySelectorAll('button');
                if (buttons.length >= 2) return container;
            }
            
            // 如果没有多按钮容器,返回第一个容器
            return containers[0];
        }
        
        return null;
    }
    
    // 插入抖音按钮
    function insertDouyinButton() {
        let buttonContainer = null;
        
        // 1. 先尝试通过XPath查找
        const selectors = [
            // 原始XPath
            '//*[@id="user_detail_element"]/div/div[2]/div[3]/div',
            // 备选1: 更通用的CSS选择器路径
            '//*[contains(@class, "user-info")]//*[contains(@class, "action")]',
            // 备选2: 查找按钮容器
            '//*[contains(@class, "profile-header")]//*[contains(@class, "button") or contains(@class, "operation")]',
            // 备选3: 关注按钮旁
            '//div[contains(@class, "following-button")]/parent::*',
            // 备选4: 按钮组合处
            '//button[contains(text(),"关注") or contains(text(),"私信") or contains(text(),"分享")]/parent::*'
        ];
        
        // 尝试每个XPath选择器
        for (const selector of selectors) {
            try {
                const result = document.evaluate(
                    selector,
                    document,
                    null,
                    XPathResult.FIRST_ORDERED_NODE_TYPE,
                    null
                );
                
                if (result && result.singleNodeValue) {
                    buttonContainer = result.singleNodeValue;
                    break;
                }
            } catch (err) { /* 忽略错误 */ }
        }
        
        // 2. 如果XPath查找失败,尝试通过DOM查找
        if (!buttonContainer) {
            buttonContainer = findDouyinButtonContainer();
        }
        
        // 3. 如果找到容器,添加按钮
        if (buttonContainer) {
            buttonContainer.appendChild(createCollectButton());
            return true;
        }
        
        return false;
    }
    
    // 插入小红书按钮
    function insertXiaohongshuButton() {
        // 尝试多种选择器
        const selectors = [
            // 原始XPath
            '//*[@id="global"]/div[1]/header/div[2]/div/div[1]',
            // 主页操作区域
            '//header//button[contains(text(), "关注") or contains(text(), "私信") or contains(@class, "follow")]/parent::*',
            // 用户资料头部区域
            '//header//div[contains(@class, "user-action")]'
        ];
        
        let buttonContainer = null;
        
        // 尝试每个选择器
        for (const selector of selectors) {
            try {
                const result = document.evaluate(
                    selector,
                    document,
                    null,
                    XPathResult.FIRST_ORDERED_NODE_TYPE,
                    null
                );
                
                if (result.singleNodeValue) {
                    buttonContainer = result.singleNodeValue;
                    break;
                }
            } catch (err) { /* 忽略错误 */ }
        }
        
        if (buttonContainer) {
            buttonContainer.appendChild(createCollectButton());
            return true;
        } else {
            setTimeout(insertXiaohongshuButton, 2000);
            return false;
        }
    }
    
    // 插入B站按钮
    function insertBilibiliButton() {
        // 检查是否已存在按钮
        if (document.querySelector('.ksr-collect-btn')) {
            return true;
        }
        
        const buttonContainer = document.evaluate(
            '//*[@id="app"]/main/div[1]/div[2]/div/div[1]/div[1]',
            document,
            null,
            XPathResult.FIRST_ORDERED_NODE_TYPE,
            null
        ).singleNodeValue;
        
        if (buttonContainer) {
            buttonContainer.appendChild(createCollectButton());
            return true;
        } else {
            setTimeout(insertBilibiliButton, 2000);
            return false;
        }
    }
    
    // 链接采集主函数
    function collectLinks() {
        if (currentHost.includes('douyin.com')) {
            collectDouyinLinks();
        } else if (currentHost.includes('xiaohongshu.com')) {
            collectXiaohongshuLinks();
        } else if (currentHost.includes('bilibili.com')) {
            collectBilibiliLinks();
        }
    }
    
    // 添加键盘快捷键 Alt+1
    document.addEventListener('keydown', function(e) {
        if (e.altKey && e.key === '1') {
            e.preventDefault();
            collectLinks();
        }
    });

    // 抖音视频链接采集
    function collectDouyinLinks() {
        const videos = [];
        
        try {
            // 主要方法
            const xpath = '//*[@id="user_detail_element"]/div//ul/li/div/a/@href';
            const result = document.evaluate(xpath, document, null, XPathResult.ANY_TYPE, null);
            
            let href;
            while (href = result.iterateNext()) {
                const link = href.value;
                
                // 排除非视频链接
                if (link.includes('/note/')) continue;
                
                // 完整化链接
                if (link.startsWith('/video/')) {
                    videos.push('https://www.douyin.com' + link);
                }
            }
            
            // 如果没有找到视频,尝试备用方法
            if (videos.length === 0) {
                // 通过视频卡片元素定位
                const videoCards = document.querySelectorAll('a[href*="/video/"]');
                videoCards.forEach(card => {
                    let href = card.getAttribute('href');
                    if (href && href.startsWith('/video/')) {
                        videos.push('https://www.douyin.com' + href);
                    }
                });
            }
        } catch (err) { /* 忽略错误 */ }

        if (videos.length > 0) {
            copyToClipboard(videos.join('\n'));
        } else {
            alert('未找到视频链接');
        }
    }

    // 小红书视频链接采集
    function collectXiaohongshuLinks() {
        let videos = [];
        
        try {
            // 方法1: 通过视频区块查找
            const videoSections = document.evaluate(
                "//section[.//span[contains(@class, 'play-icon')]]",
                document,
                null,
                XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
                null
            );
            
            // 从每个视频区块中提取链接
            for (let i = 0; i < videoSections.snapshotLength; i++) {
                const section = videoSections.snapshotItem(i);
                const linkNode = section.querySelector('a[href]');
                
                if (linkNode) {
                    let link = linkNode.getAttribute('href');
                    
                    // 完整化链接
                    if (link.startsWith('/')) {
                        link = 'https://www.xiaohongshu.com' + link;
                    }
                    
                    videos.push(link);
                }
            }
            
            // 方法2: 如果没有找到视频,尝试备用方法
            if (videos.length === 0) {
                const cards = document.querySelectorAll('a[href^="/explore/"]');
                cards.forEach(card => {
                    // 检查是否有视频标记
                    const isVideo = card.querySelector('.play-icon, .play, [class*="video"]');
                    if (isVideo) {
                        const link = 'https://www.xiaohongshu.com' + card.getAttribute('href');
                        videos.push(link);
                    }
                });
            }
        } catch (err) { /* 忽略错误 */ }

        if (videos.length > 0) {
            copyToClipboard(videos.join('\n'));
        } else {
            alert('未找到视频链接');
        }
    }

    // B站视频链接采集
    function collectBilibiliLinks() {
        const videos = [];
        
        try {
            const xpath = '//*[@id="app"]/main/div[1]/div[2]/div/div[2]/div/div/div/div/div/div/div[2]/div[1]/a/@href';
            const result = document.evaluate(xpath, document, null, XPathResult.ANY_TYPE, null);
            
            let href;
            while (href = result.iterateNext()) {
                let link = href.value;
                
                // 完整化链接
                if (link.startsWith('//')) {
                    link = 'https:' + link;
                }
                
                // 裁切链接 - 移除查询参数
                const cleanLink = link.split('?')[0];
                
                videos.push(cleanLink);
            }
        } catch (err) { /* 忽略错误 */ }

        if (videos.length > 0) {
            copyToClipboard(videos.join('\n'));
        } else {
            alert('未找到视频链接');
        }
    }
    
    // 抖音特殊处理:尝试强制重试
    let douyinRetryCount = 0;
    const MAX_DOUYIN_RETRIES = 10;
    
    function douyinFallbackCheck() {
        // 如果当前不是抖音,跳过此步骤
        if (!currentHost.includes('douyin.com')) return;
        
        // 检查是否已经有按钮
        if (document.querySelector('.ksr-collect-btn')) return;
        
        // 尝试再次插入
        if (douyinRetryCount < MAX_DOUYIN_RETRIES) {
            const success = insertDouyinButton();
            
            // 如果成功,不再尝试
            if (success) return;
            
            // 增加重试次数,下次再尝试
            douyinRetryCount++;
            setTimeout(douyinFallbackCheck, 2000);
        } else {
            // 超过重试次数,启用备用悬浮按钮
            createFloatButton();
        }
    }
    
    // 初始化
    function initScript() {
        // 清除可能存在的旧按钮
        const oldBtn = document.querySelector('.ksr-collect-btn');
        if (oldBtn) oldBtn.remove();
        
        // 删除可能存在的旧浮动按钮
        const oldFloatBtn = document.querySelector('.ksr-float-btn');
        if (oldFloatBtn) oldFloatBtn.remove();
        
        // 插入按钮
        setTimeout(insertButton, 1000);
        
        // 专门为抖音增加延迟检测
        if (currentHost.includes('douyin.com')) {
            setTimeout(douyinFallbackCheck, 3000);
        }
    }
    
    // 监听DOM变化
    function observeDOMChanges() {
        const observer = new MutationObserver(() => {
            // 每次DOM变化后检查是否需要重新插入按钮
            if (!document.querySelector('.ksr-collect-btn') && !document.querySelector('.ksr-float-btn')) {
                clearTimeout(window.insertBtnTimer);
                window.insertBtnTimer = setTimeout(insertButton, 1000);
                
                // 为抖音特别处理
                if (currentHost.includes('douyin.com')) {
                    setTimeout(douyinFallbackCheck, 1500);
                }
            }
        });
        
        // 开始观察整个document
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }
    
    // 脚本初始化
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initScript);
    } else {
        initScript();
    }
    
    // 添加DOM变化观察
    setTimeout(observeDOMChanges, 2000);
})();