Greasy Fork

pt站选种插件

在表格第一行插入一个按钮并通过右键弹出下拉框实现全选、取消全选和复制URL的功能,左键直接复制所有选中的URL

当前为 2025-06-27 提交的版本,查看 最新版本

// ==UserScript==
// @name         pt站选种插件
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  在表格第一行插入一个按钮并通过右键弹出下拉框实现全选、取消全选和复制URL的功能,左键直接复制所有选中的URL
// @author       joshua2117
// @match        https://audiences.me/*
// @match        https://hhanclub.top/*
// @match        https://ptchdbits.co/*
// @match        https://next.m-team.cc/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // 站点配置
    const siteConfigs = {
        观众: {
            rowSelector: 'table.torrents-table > tbody > tr',
            urlFetcher: async function(row) {
                const downloadImg = row.querySelector('.torrentname table tbody tr:first-child td:nth-child(2) img.download');
                if (downloadImg) {
                    const aTag = downloadImg.closest('a');
                    if (aTag) {
                        const idMatch = aTag.href.match(/id=(\d+)/);
                        if (idMatch && idMatch[1]) {
                            const id = idMatch[1];
                            try {
                                const response = await fetch(`https://audiences.me/details.php?id=${id}`);
                                if (!response.ok) {
                                    throw new Error(`Network response was not ok: ${response.statusText}`);
                                }
                                const text = await response.text();
                                const parser = new DOMParser();
                                const doc = parser.parseFromString(text, 'text/html');
                                const bTag = doc.getElementById('torrent_dl_url');
                                if (bTag) {
                                    const dlUrlATag = bTag.querySelector('a');
                                    if (dlUrlATag) {
                                        return dlUrlATag.href;
                                    } else {
                                        console.log('A 标签未找到在 torrent_dl_url b 标签内,ID:', id);
                                    }
                                } else {
                                    console.log('torrent_dl_url b 标签未找到,ID:', id);
                                }
                            } catch (err) {
                                console.error('获取详情页失败,ID:', id, err);
                            }
                        } else {
                            console.log('ID 未找到在 href 中:', aTag.href);
                        }
                    } else {
                        console.log('下载链接 A 标签未找到,在行:', row);
                    }
                } else {
                    console.log('下载图片未找到,在行:', row);
                }
                return null;
            },
            insertCheckbox: function(row) {
                const checkboxCell = document.createElement('div');
                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkboxCell.appendChild(checkbox);
                row.insertBefore(checkboxCell, row.firstElementChild);
            },
            getHeaderRow: function(rows) {
                return rows[0];
            },
            skipFirstRowForCheckboxes: true
        },
        憨憨: {
            rowSelector: '.torrent-table-sub-info',
            urlFetcher: async function(row) {
                const manageDiv = row.querySelector('div.torrent-manage'); // 获取当前行下的 class 为 torrent-manage 的 div 标签
                if (manageDiv) {
                    const downloadLink = manageDiv.querySelectorAll('a')[1];
                    if (downloadLink) {
                        return downloadLink.href;
                    } else {
                        console.log('下载链接未找到,在行:', row);
                    }
                } else {
                    console.log('torrent-manage div 未找到,在行:', row);
                }
                return null;
            },
            insertCheckbox: function(row) {
                const checkboxCell = document.createElement('div');
                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkboxCell.appendChild(checkbox);
                row.insertBefore(checkboxCell, row.firstElementChild);
            },
            getHeaderRow: function(rows) {
                const xpath = '//*[@id="mainContent"]/div/div[2]/div[1]/div';
                const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
                const element = result.singleNodeValue;
                return element;
            },
            skipFirstRowForCheckboxes: false
        },
        彩虹岛: {
            rowSelector: '.torrents > tbody > tr',
            urlFetcher: async function(row) {
                const downloadImg = row.querySelector('.torrentname > tbody > tr:first-child > td:nth-child(2) > a');
                if (downloadImg) {
                    const aTag = downloadImg.closest('a');
                    if (aTag) {
                        const idMatch = aTag.href.match(/id=(\d+)/);
                        if (idMatch && idMatch[1]) {
                            const id = idMatch[1];
                            try {
                                const response = await fetch(`https://ptchdbits.co/details.php?id=${id}`);
                                if (!response.ok) {
                                    throw new Error(`Network response was not ok: ${response.statusText}`);
                                }
                                const text = await response.text();
                                const parser = new DOMParser();
                                const doc = parser.parseFromString(text, 'text/html');
                                const bTag = doc.getElementById('outer');
                                if (bTag) {
                                    const dlUrlATag = bTag.querySelectorAll('.details >tbody >tr > .rowfollow >a')[4]
                                    if (dlUrlATag) {
                                        return dlUrlATag.href;
                                    } else {
                                        console.log('A 标签未找到在 torrent_dl_url b 标签内,ID:', id);
                                    }
                                } else {
                                    console.log('torrent_dl_url b 标签未找到,ID:', id);
                                }
                            } catch (err) {
                                console.error('获取详情页失败,ID:', id, err);
                            }
                        } else {
                            console.log('ID 未找到在 href 中:', aTag.href);
                        }
                    } else {
                        console.log('下载链接 A 标签未找到,在行:', row);
                    }
                } else {
                    console.log('下载图片未找到,在行:', row);
                }
                return null;
            },
            insertCheckbox: function(row) {
                const checkboxCell = document.createElement('div');
                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkboxCell.appendChild(checkbox);
                row.insertBefore(checkboxCell, row.firstElementChild);
            },
            getHeaderRow: function(rows) {
                return rows[0];
            },
            skipFirstRowForCheckboxes: true
        },
        馒头: {
            isDelay: true,
            rowSelector: '.ant-spin-container > div > table > tbody > tr',
            urlFetcher: async function(row) {
                const downloadImg = row.querySelector('td:nth-child(2) > div > div:nth-child(2) > div');
                if (downloadImg) {
                    const aTag = downloadImg.querySelector('td:nth-child(2) > div > div:nth-child(2) > div >a');
                    if (aTag) {
                        const idMatch = aTag.href.match(/(\d+)/);
                        if (idMatch && idMatch[1]) {
                            const id = idMatch[1];
                            try {
                                const apiUrl = localStorage.getItem("apiHost") || apiUrls[Math.random() * apiUrls.length | 0].href;
                                const f = new FormData();
                                f.set("id", id);
                                const opts ={
                                    method: "POST",
                                    headers: {
                                        authorization: localStorage.getItem("auth"),
                                        visitorId: localStorage.getItem("visitorId"),
                                        did: localStorage.getItem("did"),
                                        ts: Math.floor(Date.now() / 1e3)
                                    }
                                }
                                opts.body = f;
                                const response = await fetch(apiUrl + "/torrent/genDlToken",opts);
                                if (!response.ok) {
                                    throw new Error(`Network response was not ok: ${response.statusText}`);
                                }
                                const text = await response.json();
                                console.log('接口返回数据:',text)
                                return text.data
                            } catch (err) {
                                console.error('获取详情页失败,ID:', id, err);
                            }
                        } else {
                            console.log('ID 未找到在 href 中:', aTag.href);
                        }
                    } else {
                        console.log('下载链接 A 标签未找到,在行:', row);
                    }
                }else {
                    console.log('下载图片未找到,在行:', row);
                }
                return null;
            },
            insertCheckbox: function(row) {
                const checkboxCell = document.createElement('td');
                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkboxCell.appendChild(checkbox);
                row.insertBefore(checkboxCell, row.firstElementChild);
            },
            getHeaderRow: function(rows) {
                return document.querySelector('.ant-spin-container > div > table > thead > tr');
            },
            skipFirstRowForCheckboxes: false
        }
    };

    // 获取当前站点配置
    function getCurrentSiteConfig() {
        const currentUrl = window.location.href;
        if (currentUrl.includes('audiences.me')) {
            return siteConfigs['观众'];
        } else if (currentUrl.includes('hhanclub.top')) {
            return siteConfigs['憨憨'];
        } else if (currentUrl.includes('ptchdbits.co')) {
            return siteConfigs['彩虹岛'];
        }else if (currentUrl.includes('next.m-team.cc')) {
            return siteConfigs['馒头'];
        }
        return null;
    }

    // 获取表格行
    function getTableRows(config) {
        return document.querySelectorAll(config.rowSelector);
    }

    // 创建自定义弹窗
    function createCustomAlert(message) {
        const alertBox = document.createElement('div');
        alertBox.style.position = 'fixed';
        alertBox.style.top = '50%';
        alertBox.style.left = '50%';
        alertBox.style.transform = 'translate(-50%, -50%)';
        alertBox.style.backgroundColor = 'white';
        alertBox.style.border = '1px solid #ccc';
        alertBox.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
        alertBox.style.padding = '20px';
        alertBox.style.zIndex = '9999';
        alertBox.style.fontSize = '16px';
        alertBox.style.fontFamily = 'Arial, sans-serif';
        alertBox.style.color = '#333';
        alertBox.style.borderRadius = '5px';

        const messageText = document.createElement('span');
        messageText.textContent = message;

        const closeButton = document.createElement('button');
        closeButton.textContent = '关闭';
        closeButton.style.marginLeft = '10px';
        closeButton.style.padding = '5px 10px';
        closeButton.style.border = 'none';
        closeButton.style.borderRadius = '3px';
        closeButton.style.cursor = 'pointer';
        closeButton.style.backgroundColor = '#007bff';
        closeButton.style.color = 'white';
        closeButton.style.transition = 'background-color 0.3s ease';
        closeButton.addEventListener('mouseover', () => {
            closeButton.style.backgroundColor = '#0056b3';
        });
        closeButton.addEventListener('mouseout', () => {
            closeButton.style.backgroundColor = '#007bff';
        });
        closeButton.addEventListener('click', () => {
            document.body.removeChild(alertBox);
        });

        alertBox.appendChild(messageText);
        alertBox.appendChild(closeButton);

        document.body.appendChild(alertBox);

        setTimeout(() => {
            document.body.removeChild(alertBox);
        }, 3000); // 自动关闭弹窗,3秒后消失
    }

    // 初始化功能
    function initFeatures() {
        const config = getCurrentSiteConfig();
        if (!config) {
            console.log('不支持的站点');
            return;
        }

        const rows = getTableRows(config);

        if (rows.length > 0) {
            // 创建一个按钮
            const headerRow = config.getHeaderRow(rows);
            if (headerRow) {
                const selectCell = document.createElement('th');
                selectCell.style.width = '30px';
                const dropdownButton = document.createElement('button');
                dropdownButton.textContent = '操作';
                dropdownButton.style.padding = '5px 10px';
                dropdownButton.style.border = 'none';
                dropdownButton.style.borderRadius = '3px';
                dropdownButton.style.cursor = 'pointer';
                dropdownButton.style.backgroundColor = '#007bff';
                dropdownButton.style.color = 'white';

                dropdownButton.style.transition = 'background-color 0.3s ease';
                dropdownButton.addEventListener('mouseover', () => {
                    dropdownButton.style.backgroundColor = '#0056b3';
                });
                dropdownButton.addEventListener('mouseout', () => {
                    dropdownButton.style.backgroundColor = '#007bff';
                });

                // 创建下拉菜单
                const dropdownMenu = document.createElement('div');
                dropdownMenu.style.position = 'fixed'; // 使用 fixed 定位
                dropdownMenu.style.display = 'none';
                dropdownMenu.style.backgroundColor = 'white';
                dropdownMenu.style.border = '1px solid #ccc';
                dropdownMenu.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
                dropdownMenu.style.zIndex = '9999'; // 设置更高的 z-index 确保在最顶层

                const selectAllItem = document.createElement('div');
                selectAllItem.textContent = '全选';
                selectAllItem.style.padding = '5px 10px';
                selectAllItem.style.cursor = 'pointer';
                selectAllItem.addEventListener('click', () => {
                    rows.forEach((row, index) => {
                        if (index !== 0 || !config.skipFirstRowForCheckboxes) { // 根据配置决定是否跳过第一行
                            const checkbox = row.querySelector('input[type="checkbox"]');
                            if (checkbox) {
                                checkbox.checked = true;
                            }
                        }
                    });
                    dropdownMenu.style.display = 'none';
                });

                const deselectAllItem = document.createElement('div');
                deselectAllItem.textContent = '取消全选';
                deselectAllItem.style.padding = '5px 10px';
                deselectAllItem.style.cursor = 'pointer';
                deselectAllItem.addEventListener('click', () => {
                    rows.forEach((row, index) => {
                        if (index !== 0 || !config.skipFirstRowForCheckboxes) { // 根据配置决定是否跳过第一行
                            const checkbox = row.querySelector('input[type="checkbox"]');
                            if (checkbox) {
                                checkbox.checked = false;
                            }
                        }
                    });
                    dropdownMenu.style.display = 'none';
                });

                const copyButtonItem = document.createElement('div');
                copyButtonItem.textContent = '复制URL';
                copyButtonItem.style.padding = '5px 10px';
                copyButtonItem.style.cursor = 'pointer';
                copyButtonItem.addEventListener('click', async () => {
                    await performCopyUrls();
                    dropdownMenu.style.display = 'none';
                });

                dropdownMenu.appendChild(selectAllItem);
                dropdownMenu.appendChild(deselectAllItem);
                dropdownMenu.appendChild(copyButtonItem);

                // 将按钮和下拉菜单添加到表头第一列
                selectCell.appendChild(dropdownButton);
                selectCell.appendChild(dropdownMenu);
                headerRow.insertBefore(selectCell, headerRow.firstElementChild);

                // 显示和隐藏下拉菜单
                dropdownButton.addEventListener('contextmenu', (e) => {
                    e.preventDefault();
                    showDropdownMenu(e);
                });

                document.addEventListener('click', (e) => {
                    if (!dropdownMenu.contains(e.target)) {
                        dropdownMenu.style.display = 'none';
                    }
                });

                // 左键点击按钮直接复制所有选中的URL
                dropdownButton.addEventListener('click', async (e) => {
                    if (e.button === 0) { // 左键点击
                        await performCopyUrls();
                    }
                });

                // 复制URL的函数
                async function performCopyUrls() {
                    const checkedRows = Array.from(rows).filter((row, index) => {
                        if (index === 0 && config.skipFirstRowForCheckboxes) {
                            return false; // 跳过第一行
                        }
                        const checkbox = row.querySelector('input[type="checkbox"]');
                        return checkbox && checkbox.checked;
                    });
                    let urls = [];
                    let processedCount = 0;

                    // 禁用按钮并更改文本和样式
                    dropdownButton.textContent = `正在复制... (${processedCount}/${checkedRows.length})`;
                    dropdownButton.style.backgroundColor = '#cccccc';
                    dropdownButton.style.pointerEvents = 'none'; // 禁用点击事件

                    for (const [index, row] of checkedRows.entries()) {
                        const url = await config.urlFetcher(row);
                        if (url) {
                            urls.push(url);
                        }
                        processedCount++;
                        dropdownButton.textContent = `正在复制... (${processedCount}/${checkedRows.length})`;
                    }

                    if (urls.length > 0) {
                        navigator.clipboard.writeText(urls.join('\n'))
                            .then(() => {
                                console.log('URL 已复制到剪贴板:', urls.join('\n'));
                                createCustomAlert('已复制完成');
                            })
                            .catch(err => {
                                console.error('复制 URL 失败:', err);
                            });
                    } else {
                        console.log('没有选择要复制的 URL。');
                    }

                    // 恢复按钮的状态
                    dropdownButton.textContent = '操作';
                    dropdownButton.style.backgroundColor = '#007bff';
                    dropdownButton.style.pointerEvents = 'auto'; // 启用点击事件
                }

                // 显示下拉菜单的函数
                function showDropdownMenu(event) {
                    const rect = event.target.getBoundingClientRect();
                    dropdownMenu.style.left = `${rect.right}px`; // 出现在按钮右侧
                    dropdownMenu.style.top = `${rect.bottom}px`; // 出现在按钮下方
                    dropdownMenu.style.display = 'block';
                }
            }

            // 在每个<tr>元素的第一<td>前插入复选框(根据配置决定是否跳过第一行)
            rows.forEach((row, index) => {
                if (!(index === 0 && config.skipFirstRowForCheckboxes)) { // 根据配置决定是否跳过第一行
                    config.insertCheckbox(row);
                }
            });
        } else {
            console.log('表格未找到。');
        }
    }

    function waitForElement(selector, callback) {
        const observer = new MutationObserver((mutations) => {
            mutations.forEach(() => {
                const element = document.querySelector(selector);
                if (element) {
                    observer.disconnect();
                    callback(element);
                }
            });
        });

        observer.observe(document.body, { childList: true, subtree: true });
    }


    const siteconfig = getCurrentSiteConfig();
    let isInit = false;
    if(siteconfig.isDelay){
        waitForElement(siteconfig.rowSelector, function (el) {
            if(!isInit){
                console.log('表格元素已加载:', el);
                // 初始化功能
                setTimeout(initFeatures, 1000); // 可选延迟
                isInit = true;
            }
        });
    }else{
        // 初始化功能
        initFeatures();
    }

})();