Greasy Fork

Greasy Fork is available in English.

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

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

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