Greasy Fork

FSM 收藏夹链接提取

Extract and convert FSM torrent links from all pages (on-demand authentication)

// ==UserScript==
// @name         FSM 收藏夹链接提取
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  Extract and convert FSM torrent links from all pages (on-demand authentication)
// @author       You
// @match        https://fsm.name/Torrents/mine?type=favorite*
// @grant        GM_setClipboard
// ==/UserScript==

(function() {
    'use strict';

    let passkey = '';
    let authorization = '';
    let deviceId = '';
    let maxPage = 1;
    let currentPage = 1;
    let allLinks = [];

    // 创建加载覆盖层
    function createLoadingOverlay() {
        const overlay = document.createElement('div');
        overlay.id = 'fsm-loading-overlay';
        overlay.style.position = 'fixed';
        overlay.style.top = '0';
        overlay.style.left = '0';
        overlay.style.width = '100%';
        overlay.style.height = '100%';
        overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
        overlay.style.zIndex = '10000';
        overlay.style.display = 'flex';
        overlay.style.flexDirection = 'column';
        overlay.style.justifyContent = 'center';
        overlay.style.alignItems = 'center';
        overlay.style.color = 'white';
        overlay.style.fontSize = '24px';

        const loadingText = document.createElement('div');
        loadingText.id = 'fsm-loading-text';
        loadingText.textContent = '正在获取下载链接...';
        loadingText.style.marginBottom = '20px';

        const progressText = document.createElement('div');
        progressText.id = 'fsm-progress-text';
        progressText.textContent = '进度: 0/0 页';
        progressText.style.fontSize = '18px';

        overlay.appendChild(loadingText);
        overlay.appendChild(progressText);
        document.body.appendChild(overlay);

        return overlay;
    }

    // 获取凭证信息
    function getCredentials() {
        authorization = localStorage.getItem('token');
        deviceId = localStorage.getItem('DeviceId');

        if (!authorization || !deviceId) {
            showNotification('获取凭证失败,请确保已登录', 'error');
            return false;
        }

        return true;
    }

    // 获取 passkey
    async function fetchPasskey() {
        try {
            if (!getCredentials()) return false;

            const response = await fetch('https://fsm.name/api/Users/infos', {
                headers: {
                    'accept': 'application/json, text/plain, */*',
                    'authorization': authorization,
                    'deviceid': deviceId
                }
            });

            const data = await response.json();
            if (data.success && data.data.passkey) {
                passkey = data.data.passkey;
                return true;
            }

            showNotification('获取 passkey 失败: ' + (data.msg || '未知错误'), 'error');
            return false;
        } catch (error) {
            console.error('获取 passkey 失败:', error);
            showNotification('获取 passkey 失败: ' + error.message, 'error');
            return false;
        }
    }

    // 获取收藏种子列表
    async function fetchFavorites(page) {
        try {
            if (!getCredentials()) return null;

            const response = await fetch(`https://fsm.name/api/Torrents/listMyFavorite?page=${page}`, {
                headers: {
                    'accept': 'application/json, text/plain, */*',
                    'authorization': authorization,
                    'deviceid': deviceId
                }
            });

            const data = await response.json();
            if (data.success) {
                // 更新总页数
                if (data.data.maxPage > maxPage) {
                    maxPage = data.data.maxPage;
                }

                return data.data.list || [];
            }

            showNotification('获取收藏列表失败: ' + (data.msg || '未知错误'), 'error');
            return null;
        } catch (error) {
            console.error('获取收藏列表失败:', error);
            showNotification('获取收藏列表失败: ' + error.message, 'error');
            return null;
        }
    }

    // 更新进度文本
    function updateProgress(current, total) {
        const progressText = document.getElementById('fsm-progress-text');
        if (progressText) {
            progressText.textContent = `进度: ${current}/${total} 页`;
        }
    }

    // 显示通知
    function showNotification(message, type) {
        if (window.$notify) {
            window.$notify({
                message: message,
                type: type
            });
        } else {
            alert(message);
        }
    }

    // 获取当前页面的种子链接
    async function extractCurrentPageLinks() {
        if (!passkey) {
            const success = await fetchPasskey();
            if (!success) return [];
        }

        const links = document.querySelectorAll('.el-table__body-wrapper a');
        return Array.from(links)
            .map(link => {
                const tid = new URL(link.href).searchParams.get('tid');
                return `https://api.fsm.name/Torrents/download?tid=${tid}&passkey=${passkey}&source=direct`;
            });
    }

    // 提取单页的链接
    function extractLinksFromData(list) {
        return list.map(item => {
            return `https://api.fsm.name/Torrents/download?tid=${item.tid}&passkey=${passkey}&source=direct`;
        });
    }

    // 获取所有页面的链接
    async function extractAllPagesLinks() {
        const overlay = createLoadingOverlay();

        try {
            // 在按钮点击时获取 passkey
            if (!passkey) {
                const success = await fetchPasskey();
                if (!success) {
                    overlay.remove();
                    return;
                }
            }

            // 获取第一页数据,确定总页数
            const firstPageData = await fetchFavorites(1);
            if (!firstPageData) {
                overlay.remove();
                return;
            }

            // 添加第一页链接
            allLinks = extractLinksFromData(firstPageData);
            updateProgress(1, maxPage);

            // 获取剩余页面
            for (let page = 2; page <= maxPage; page++) {
                const pageData = await fetchFavorites(page);
                if (pageData) {
                    const pageLinks = extractLinksFromData(pageData);
                    allLinks = allLinks.concat(pageLinks);
                }

                updateProgress(page, maxPage);

                // 稍微延迟,避免请求过快
                await new Promise(resolve => setTimeout(resolve, 300));
            }

            // 复制到剪贴板
            GM_setClipboard(allLinks.join('\n'));
            showNotification(`已复制所有收藏链接 (共 ${allLinks.length} 个)`, 'success');
        } catch (error) {
            console.error('获取所有链接失败:', error);
            showNotification('获取所有链接失败: ' + error.message, 'error');
        } finally {
            overlay.remove();
        }
    }

    // 复制当前页面链接
    async function extractAndCopyCurrentPageLinks() {
        try {
            // 在按钮点击时获取 passkey
            if (!passkey) {
                const success = await fetchPasskey();
                if (!success) return;
            }
            
            const links = await extractCurrentPageLinks();

            if (links.length > 0) {
                GM_setClipboard(links.join('\n'));
                showNotification(`已复制当前页面链接 (共 ${links.length} 个)`, 'success');
            } else {
                showNotification('没有找到可复制的链接', 'warning');
            }
        } catch (error) {
            console.error('提取当前页链接失败:', error);
            showNotification('提取当前页链接失败: ' + error.message, 'error');
        }
    }

    // 添加按钮
    function addButtons() {
        // 检查是否已存在按钮
        if (document.getElementById('fsm-extract-buttons')) {
            return;
        }

        // 创建按钮容器
        const buttonContainer = document.createElement('div');
        buttonContainer.id = 'fsm-extract-buttons';
        buttonContainer.style.position = 'fixed';
        buttonContainer.style.right = '20px';
        buttonContainer.style.bottom = '20px';
        buttonContainer.style.zIndex = '9999';
        buttonContainer.style.display = 'flex';
        buttonContainer.style.flexDirection = 'column';
        buttonContainer.style.gap = '10px';

        // 当前页面按钮
        const currentPageBtn = document.createElement('button');
        currentPageBtn.className = 'el-button el-button--primary el-button--small';
        currentPageBtn.innerHTML = '提取当前页链接';
        currentPageBtn.addEventListener('click', extractAndCopyCurrentPageLinks);

        // 所有页面按钮
        const allPagesBtn = document.createElement('button');
        allPagesBtn.className = 'el-button el-button--danger el-button--small';
        allPagesBtn.innerHTML = '提取所有页链接';
        allPagesBtn.addEventListener('click', extractAllPagesLinks);

        // 添加到容器
        buttonContainer.appendChild(currentPageBtn);
        buttonContainer.appendChild(allPagesBtn);

        // 添加到页面
        document.body.appendChild(buttonContainer);
    }

    // 使用 MutationObserver 确保在页面动态加载后添加按钮
    const observer = new MutationObserver((mutations, obs) => {
        if (document.querySelector('.el-table__body-wrapper')) {
            addButtons();
        }
    });

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

    // 页面加载完成时也尝试添加按钮
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', addButtons);
    } else {
        addButtons();
    }

    // 移除了初始获取 passkey 的部分
})();