JavDB 推送115离线

复制和下载按旁添加新按钮,离线路径,按钮名字可使用ui选择自定义,按钮2可开关,使用115web接口实现,支持并适配移动端与pc端

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         JavDB 推送115离线
// @namespace    javdb.button.empty
// @version      0.4
// @description  复制和下载按旁添加新按钮,离线路径,按钮名字可使用ui选择自定义,按钮2可开关,使用115web接口实现,支持并适配移动端与pc端
// @match        https://javdb561.com/*
// @match        https://javdb.com/*
// @icon         https://javdb561.com/favicon.ico
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @run-at       document-end
// @author       楠
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';
    const TARGET_CLASS = "x-empty-btn";

    let btn1Name = GM_getValue('btn1_name', '按钮1');
    let btn2Name = GM_getValue('btn2_name', '按钮2');
    let btn2Enabled = GM_getValue('btn2_enabled', false);
    let btn1Cid = GM_getValue('btn1_cid', '');
    let btn2Cid = GM_getValue('btn2_cid', '');
    let cookie = GM_getValue('cookie', '');

    function handleMagnetLink(magnet, targetCid, buttonName) {
        console.log(`[${buttonName}] 捕获磁力链接:`, magnet);
        console.log(`[${buttonName}] 目标CID:`, targetCid);

        if (!targetCid) {
            showToast(`❌ 请先为【${buttonName}】设置目标文件夹`, 3000);
            showSettingsModal();
            return;
        }

        if (!cookie) {
            showToast('❌ 请先设置115 Cookie', 3000);
            showSettingsModal();
            return;
        }

        GM_xmlhttpRequest({
            method: "POST",
            url: "https://115.com/web/lixian/?ct=lixian&ac=add_task_url",
            headers: {
                "Cookie": cookie,
                "Content-Type": "application/x-www-form-urlencoded",
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
                "Referer": "https://115.com/"
            },
            data: `url=${encodeURIComponent(magnet)}&wp_path_id=${encodeURIComponent(targetCid)}`,
            onload: function(response) {
                try {
                    const result = JSON.parse(response.responseText);
                    console.log('115 API响应:', result);
                    
                    if (result.state === true) {
                        showToast(`✅ 【${buttonName}】离线任务提交成功`);
                    } else if (result.error_msg && result.error_msg.includes("任务已存在")) {
                        showToast(`⚠️ 任务已存在,请勿输入重复的链接地址`, 3000, 'warning');
                    } else {
                        showToast(`❌ 【${buttonName}】提交失败: ${result.error_msg || '未知错误'}`);
                    }
                } catch (error) {
                    console.error('解析响应失败:', error);
                    showToast('❌ 解析115响应失败');
                }
            },
            onerror: function(error) {
                console.error('115请求错误:', error);
                showToast('❌ 115请求错误');
            }
        });
    }

    function generateButtonsHTML() {
        let html = `<button class="${TARGET_CLASS} button is-small x-un-hover is-primary">${btn1Name}</button>`;
        if (btn2Enabled) {
            html += `<button class="${TARGET_CLASS} button is-small x-un-hover is-link">${btn2Name}</button>`;
        }
        return html;
    }

    function insertToMagnets() {
        const magnetsNode = document.querySelector("#magnets-content");
        if (!magnetsNode) return;

        magnetsNode.querySelectorAll(".item.columns").forEach((node) => {
            const btns = node.querySelector(".buttons.column");
            if (btns && !btns.querySelector(`.${TARGET_CLASS}`)) {
                btns.innerHTML += generateButtonsHTML();
                
                const btn1 = btns.querySelector(`.${TARGET_CLASS}.is-primary`);
                if (btn1) {
                    btn1.onclick = () => {
                        const aTag = node.querySelector('a[href^="magnet:?"]');
                        if (aTag) {
                            handleMagnetLink(aTag.getAttribute('href'), btn1Cid, btn1Name);
                        } else {
                            showToast("❌ 未找到磁力链接");
                        }
                    };
                }
                
                if (btn2Enabled) {
                    const btn2 = btns.querySelector(`.${TARGET_CLASS}.is-link`);
                    if (btn2) {
                        btn2.onclick = () => {
                            const aTag = node.querySelector('a[href^="magnet:?"]');
                            if (aTag) {
                                handleMagnetLink(aTag.getAttribute('href'), btn2Cid, btn2Name);
                            } else {
                                showToast("❌ 未找到磁力链接");
                            }
                        };
                    }
                }
            }
        });
    }

    insertToMagnets();

    const observer = new MutationObserver(insertToMagnets);
    observer.observe(document.body, { childList: true, subtree: true });

    function showToast(message, duration = 2500, type = 'default') {
        const toast = document.createElement('div');
        
        let bgGradient;
        if (type === 'warning') {
            bgGradient = 'linear-gradient(135deg, #ff9800, #f57c00)';
        } else if (message.includes('❌') || message.includes('失败') || message.includes('错误')) {
            bgGradient = 'linear-gradient(135deg, #ff5252, #b71c1c)';
        } else if (message.includes('✅') || message.includes('成功')) {
            bgGradient = 'linear-gradient(135deg, #4CAF50, #2E7D32)';
        } else {
            bgGradient = 'linear-gradient(135deg, #2196F3, #1976D2)';
        }

        Object.assign(toast.style, {
            position: 'fixed',
            top: '110px',
            right: '20px',
            padding: '16px 24px',
            background: bgGradient,
            color: '#fff',
            borderRadius: '12px',
            boxShadow: '0 6px 20px rgba(0,0,0,0.2)',
            fontSize: '14px',
            fontWeight: '500',
            opacity: '0',
            transform: 'translateX(100%) scale(0.9)',
            transition: 'all 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55)',
            zIndex: 10000,
            maxWidth: '300px',
            backdropFilter: 'blur(10px)',
            border: '1px solid rgba(255,255,255,0.1)',
            overflow: 'hidden'
        });

        const progressBar = document.createElement('div');
        Object.assign(progressBar.style, {
            position: 'absolute',
            bottom: '0',
            left: '0',
            height: '3px',
            background: 'linear-gradient(90deg, rgba(255,255,255,0.5), rgba(255,255,255,0.8))',
            width: '100%',
            transform: 'scaleX(1)',
            transformOrigin: 'left center',
            transition: 'transform linear',
            borderRadius: '0 0 12px 12px'
        });
        toast.appendChild(progressBar);

        const icon = document.createElement('span');
        if (type === 'warning') {
            icon.innerHTML = '⚠️';
        } else if (message.includes('❌') || message.includes('失败') || message.includes('错误')) {
            icon.innerHTML = '❌';
        } else if (message.includes('✅') || message.includes('成功')) {
            icon.innerHTML = '✓';
        } else {
            icon.innerHTML = 'ℹ️';
        }
        
        Object.assign(icon.style, {
            display: 'inline-block',
            marginRight: '10px',
            fontSize: '16px',
            fontWeight: 'bold',
            verticalAlign: 'middle'
        });

        const textSpan = document.createElement('span');
        textSpan.textContent = message;
        textSpan.style.verticalAlign = 'middle';

        toast.appendChild(icon);
        toast.appendChild(textSpan);
        document.body.appendChild(toast);

        requestAnimationFrame(() => {
            toast.style.opacity = '1';
            toast.style.transform = 'translateX(0) scale(1)';
            progressBar.style.transition = `transform ${duration}ms linear`;
            progressBar.style.transform = 'scaleX(0)';
        });

        setTimeout(() => {
            toast.style.opacity = '0';
            toast.style.transform = 'translateX(100%) scale(0.9)';
            setTimeout(() => {
                if (toast.parentNode) toast.parentNode.removeChild(toast);
            }, 500);
        }, duration);
    }

    async function getFolders(cid = 0) {
        if (!cookie) {
            showToast('请先设置Cookie');
            return [];
        }

        try {
            const response = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: "GET",
                    url: `https://webapi.115.com/files?aid=1&cid=${cid}&show_dir=1`,
                    headers: { "Cookie": cookie, "User-Agent": "Mozilla/5.0" },
                    onload: resolve,
                    onerror: reject
                });
            });
            const data = JSON.parse(response.responseText);
            if (data.state && data.data) {
                return data.data.filter(item => item.fl && item.fl.length === 0)
                    .map(item => ({ name: item.n, cid: item.cid }));
            }
            return [];
        } catch (error) {
            console.error('获取文件夹列表失败:', error);
            showToast('获取文件夹列表失败');
            return [];
        }
    }

    function showSettingsModal() {
        if (document.querySelector('#tm-settings-modal')) return;

        const overlay = document.createElement('div');
        overlay.id = 'tm-settings-modal';
        Object.assign(overlay.style, { 
            position: 'fixed', 
            top: '0', 
            left: '0', 
            width: '100%', 
            height: '100%', 
            background: 'rgba(0,0,0,0.5)', 
            zIndex: 10001, 
            display: 'flex', 
            justifyContent: 'center', 
            alignItems: 'center' 
        });

        const modal = document.createElement('div');
        Object.assign(modal.style, { 
            background: '#fff', 
            padding: '20px 25px', 
            borderRadius: '10px', 
            width: '500px', 
            boxShadow: '0 6px 20px rgba(0,0,0,0.3)', 
            fontFamily: 'Arial, sans-serif', 
            maxHeight: '80vh', 
            overflowY: 'auto' 
        });

        modal.innerHTML = `
            <h3 style="margin-top:0;margin-bottom:15px;color:#333">115 设置</h3>
            <div style="margin-bottom:10px;">
                <label style="display:block;margin-bottom:5px;color:#555;">Cookie:</label>
                <div style="display:flex;align-items:center;gap:5px;">
                    <input id="tm-cookie-input" type="password" value="${cookie}" style="flex:1;padding:6px;border:1px solid #ccc;border-radius:4px;">
                    <button id="tm-toggle-cookie" style="padding:6px 10px;border:1px solid #ccc;border-radius:4px;background:#f5f5f5;cursor:pointer;">显示</button>
                </div>
            </div>
            
            <div style="margin-bottom:15px;padding:10px;background:#f9f9f9;border-radius:5px;">
                <h4 style="margin-top:0;color:#333;border-bottom:1px solid #eee;padding-bottom:5px;">${btn1Name} 设置</h4>
                <div style="margin-bottom:10px;">
                    <label style="display:block;margin-bottom:5px;color:#555;">按钮名称:</label>
                    <input id="tm-btn1-name" type="text" value="${btn1Name}" style="width:100%;padding:6px;border:1px solid #ccc;border-radius:4px;">
                </div>
                <div style="margin-bottom:10px;">
                    <label style="display:block;margin-bottom:5px;color:#555;">目标文件夹 CID:</label>
                    <div style="display:flex;gap:10px;">
                        <input id="tm-btn1-cid-input" type="text" value="${btn1Cid}" style="flex:1;padding:6px;border:1px solid #ccc;border-radius:4px;" placeholder="未设置">
                        <button id="tm-btn1-browse-folders" style="padding:6px 12px;border:none;border-radius:4px;background:#2196F3;color:#fff;cursor:pointer;">浏览文件夹</button>
                    </div>
                </div>
            </div>
            
            <div style="margin-bottom:15px;padding:10px;background:#f9f9f9;border-radius:5px;">
                <h4 style="margin-top:0;color:#333;border-bottom:1px solid #eee;padding-bottom:5px;">${btn2Name} 设置</h4>
                <div style="margin-bottom:10px;">
                    <label style="display:block;margin-bottom:5px;color:#555;">按钮名称:</label>
                    <input id="tm-btn2-name" type="text" value="${btn2Name}" style="width:100%;padding:6px;border:1px solid #ccc;border-radius:4px;">
                </div>
                <div style="margin-bottom:10px;">
                    <label style="display:block;margin-bottom:5px;color:#555;">目标文件夹 CID:</label>
                    <div style="display:flex;gap:10px;">
                        <input id="tm-btn2-cid-input" type="text" value="${btn2Cid}" style="flex:1;padding:6px;border:1px solid #ccc;border-radius:4px;" placeholder="未设置">
                        <button id="tm-btn2-browse-folders" style="padding:6px 12px;border:none;border-radius:4px;background:#2196F3;color:#fff;cursor:pointer;">浏览文件夹</button>
                    </div>
                </div>
                <label style="display:inline-flex;align-items:center;">
                    <input type="checkbox" id="tm-btn2-enabled" ${btn2Enabled ? 'checked' : ''} style="margin-right:5px;"> 显示按钮2
                </label>
            </div>
            
            <div style="text-align:right;">
                <button id="tm-settings-cancel" style="margin-right:10px;padding:6px 12px;border:none;border-radius:4px;background:#ccc;color:#fff;cursor:pointer;">取消</button>
                <button id="tm-settings-save" style="padding:6px 12px;border:none;border-radius:4px;background:#4CAF50;color:#fff;cursor:pointer;">保存</button>
            </div>
        `;

        overlay.appendChild(modal);
        document.body.appendChild(overlay);

        const cookieInput = overlay.querySelector('#tm-cookie-input');
        const toggleButton = overlay.querySelector('#tm-toggle-cookie');
        
        toggleButton.addEventListener('click', function() {
            if (cookieInput.type === 'password') {
                cookieInput.type = 'text';
                toggleButton.textContent = '隐藏';
            } else {
                cookieInput.type = 'password';
                toggleButton.textContent = '显示';
            }
        });

        overlay.querySelector('#tm-btn1-browse-folders').onclick = () => {
            const cookieValue = document.querySelector('#tm-cookie-input').value.trim();
            if (!cookieValue) {
                showToast('请先设置Cookie');
                return;
            }
            GM_setValue('cookie', cookieValue);
            cookie = cookieValue;
            showFolderBrowser('btn1');
        };

        overlay.querySelector('#tm-btn2-browse-folders').onclick = () => {
            const cookieValue = document.querySelector('#tm-cookie-input').value.trim();
            if (!cookieValue) {
                showToast('请先设置Cookie');
                return;
            }
            GM_setValue('cookie', cookieValue);
            cookie = cookieValue;
            showFolderBrowser('btn2');
        };

        overlay.querySelector('#tm-settings-cancel').onclick = () => overlay.remove();

        overlay.querySelector('#tm-settings-save').onclick = () => {
            const newBtn1Name = document.querySelector('#tm-btn1-name').value.trim();
            const newBtn2Name = document.querySelector('#tm-btn2-name').value.trim();
            const newBtn1Cid = document.querySelector('#tm-btn1-cid-input').value.trim();
            const newBtn2Cid = document.querySelector('#tm-btn2-cid-input').value.trim();
            const newBtn2Enabled = document.querySelector('#tm-btn2-enabled').checked;
            const cookieValue = document.querySelector('#tm-cookie-input').value.trim();

            if (newBtn1Cid && newBtn2Cid && newBtn1Cid === newBtn2Cid && newBtn2Enabled) {
                showToast('❌ 两个按钮的目标文件夹不能相同', 3000);
                return;
            }

            GM_setValue('cookie', cookieValue);
            GM_setValue('btn1_name', newBtn1Name);
            GM_setValue('btn2_name', newBtn2Name);
            GM_setValue('btn1_cid', newBtn1Cid);
            GM_setValue('btn2_cid', newBtn2Cid);
            GM_setValue('btn2_enabled', newBtn2Enabled);

            btn1Name = newBtn1Name;
            btn2Name = newBtn2Name;
            btn1Cid = newBtn1Cid;
            btn2Cid = newBtn2Cid;
            btn2Enabled = newBtn2Enabled;
            cookie = cookieValue;

            showToast('✅ 设置已保存,按钮已更新');
            
            document.querySelectorAll(`.${TARGET_CLASS}`).forEach(btn => btn.remove());
            insertToMagnets();
            
            overlay.remove();
        };
    }

    async function showFolderBrowser(buttonType) {
        if (document.querySelector('#tm-folder-browser')) return;

        const overlay = document.createElement('div');
        overlay.id = 'tm-folder-browser';
        Object.assign(overlay.style, { 
            position: 'fixed', 
            top: '0', 
            left: '0', 
            width: '100%', 
            height: '100%', 
            background: 'rgba(0,0,0,0.5)', 
            zIndex: 10002, 
            display: 'flex', 
            justifyContent: 'center', 
            alignItems: 'center' 
        });

        const modal = document.createElement('div');
        Object.assign(modal.style, { 
            background: '#fff', 
            padding: '20px', 
            borderRadius: '10px', 
            width: '500px', 
            maxHeight: '80vh', 
            boxShadow: '0 6px 20px rgba(0,0,0,0.3)', 
            fontFamily: 'Arial, sans-serif', 
            display: 'flex', 
            flexDirection: 'column' 
        });

        modal.innerHTML = `
            <h3 style="margin-top:0;margin-bottom:15px;color:#333">浏览文件夹 - ${buttonType === 'btn1' ? btn1Name : btn2Name}</h3>
            <div id="tm-current-path" style="margin-bottom:10px;padding:8px;background:#f5f5f5;border-radius:4px;">根目录</div>
            <div id="tm-folders-list" style="flex:1;overflow-y:auto;margin-bottom:15px;min-height:200px;">
                <div style="text-align:center;padding:40px 0;">加载中...</div>
            </div>
            <div style="display:flex;justify-content:space-between;">
                <button id="tm-folder-back" style="padding:6px 12px;border:none;border-radius:4px;background:#ccc;color:#fff;cursor:pointer;">返回上级</button>
                <button id="tm-folder-cancel" style="padding:6px 12px;border:none;border-radius:4px;background:#ccc;color:#fff;cursor:pointer;">取消</button>
                <button id="tm-folder-select" style="padding:6px 12px;border:none;border-radius:4px;background:#4CAF50;color:#fff;cursor:pointer;">选择当前文件夹</button>
            </div>
        `;

        overlay.appendChild(modal);
        document.body.appendChild(overlay);

        let currentCid = 0, currentPath = ["根目录"], cidStack = [], pathStack = [];
        const targetButtonType = buttonType;

        async function loadFolders(cid = 0) {
            const foldersList = document.getElementById('tm-folders-list');
            foldersList.innerHTML = '<div style="text-align:center;padding:40px 0;">加载中...</div>';
            const folders = await getFolders(cid);
            if (!folders.length) {
                foldersList.innerHTML = '<div style="text-align:center;padding:40px 0;color:#999;">该目录下没有文件夹</div>';
                return;
            }
            foldersList.innerHTML = '';
            folders.forEach(folder => {
                const folderItem = document.createElement('div');
                folderItem.className = 'tm-folder-item';
                folderItem.style.padding = '10px';
                folderItem.style.borderBottom = '1px solid #eee';
                folderItem.style.cursor = 'pointer';
                folderItem.style.display = 'flex';
                folderItem.style.justifyContent = 'space-between';
                folderItem.style.alignItems = 'center';
                folderItem.innerHTML = `
                    <span>${folder.name}</span>
                    <span style="color:#999;font-size:12px;">CID: ${folder.cid}</span>
                `;
                folderItem.onclick = () => {
                    cidStack.push(currentCid);
                    pathStack.push([...currentPath]);
                    currentCid = folder.cid;
                    currentPath.push(folder.name);
                    updatePathDisplay();
                    loadFolders(currentCid);
                };
                foldersList.appendChild(folderItem);
            });
        }

        function updatePathDisplay() {
            document.getElementById('tm-current-path').textContent = currentPath.join(' / ');
        }

        document.getElementById('tm-folder-back').onclick = () => {
            if (cidStack.length > 0) {
                currentCid = cidStack.pop();
                currentPath = pathStack.pop();
                updatePathDisplay();
                loadFolders(currentCid);
            }
        };
        
        document.getElementById('tm-folder-cancel').onclick = () => overlay.remove();
        
        document.getElementById('tm-folder-select').onclick = () => {
            if (currentCid !== 0) {
                const cidInput = document.querySelector(`#tm-${targetButtonType}-cid-input`);
                if (cidInput) {
                    cidInput.value = currentCid;
                    
                    const otherButtonType = targetButtonType === 'btn1' ? 'btn2' : 'btn1';
                    const otherCidInput = document.querySelector(`#tm-${otherButtonType}-cid-input`);
                    const otherButtonEnabled = document.querySelector('#tm-btn2-enabled') ? document.querySelector('#tm-btn2-enabled').checked : false;
                    
                    if (otherCidInput && otherCidInput.value === cidInput.value && otherButtonEnabled) {
                        showToast('⚠️ 注意: 两个按钮的目标文件夹相同', 3000, 'warning');
                    }
                }
                showToast(`已选择: ${currentPath.join(' / ')}`);
            }
            overlay.remove();
        };

        loadFolders(currentCid);
        updatePathDisplay();
    }

    function addSettingsButton() {
        if (document.querySelector('#tm-settings-btn')) return;
        const btn = document.createElement('div');
        btn.id = 'tm-settings-btn';
        btn.textContent = '⚙️ 115设置';
        Object.assign(btn.style, { 
            position: 'fixed', 
            bottom: '25px', 
            right: '10px', 
            backgroundColor: '#2196F3', 
            color: '#fff', 
            padding: '8px 12px', 
            borderRadius: '8px', 
            cursor: 'pointer', 
            zIndex: 10000, 
            fontWeight: 'bold', 
            boxShadow: '0 4px 12px rgba(0,0,0,0.2)' 
        });
        btn.onclick = showSettingsModal;
        document.body.appendChild(btn);
    }

    addSettingsButton();
})();