简篇助手

简篇网站账号切换与媒体提取工具

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         简篇助手
// @namespace    http://tampermonkey.net/
// @version      0.5
// @description  简篇网站账号切换与媒体提取工具
// @author       Your name
// @match        https://www.jianpian.cn/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @license      MIT
// ==/UserScript==


(function() {
    'use strict';

    const STYLES = {
        floatingWindow: `
            position: fixed;
            top: 20px;
            right: 20px;
            width: 320px;
            height: 500px;
            background: rgba(255, 255, 255, 0.98);
            border-radius: 12px;
            padding: 15px;
            z-index: 9999;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            transition: all 0.3s ease;
            display: flex;
            flex-direction: column;
        `,
        tabs: `display: flex; margin-bottom: 15px; border-bottom: 1px solid #eee;`,
        tab: `padding: 8px 16px; cursor: pointer; border-bottom: 2px solid transparent; color: #666; transition: all 0.3s ease;`,
        activeTab: `color: #2c5282; border-bottom: 2px solid #2c5282;`,
        button: `
            background: #4299e1 !important;
            color: white !important;
            border: none !important;
            padding: 8px 12px !important;
            border-radius: 4px !important;
            cursor: pointer !important;
            font-size: 13px !important;
            font-weight: 500 !important;
            transition: all 0.2s ease !important;
            width: 100% !important;
            height: auto !important;
            line-height: 1.5 !important;
            box-sizing: border-box !important;
            margin: 0 !important;
            text-align: center !important;
            display: block !important;
        `,
        content: `height: 100%; overflow-y: auto; padding: 10px;`,
        minimizeButton: `
            position: absolute;
            top: 15px;
            right: 15px;
            width: 20px;
            height: 20px;
            line-height: 20px;
            text-align: center;
            cursor: pointer;
            font-size: 16px;
            color: #666;
            transition: all 0.3s ease;
        `,
        minimized: `
            position: fixed;
            top: 20%;
            right: 20px;
            width: auto;
            height: auto;
            padding: 10px 15px;
            background: rgba(255, 255, 255, 0.98);
            border-radius: 12px;
            z-index: 9999;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
            cursor: pointer;
            transition: all 0.3s ease;
        `,
        modal: `
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.5);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 10000;
        `,
        modalContent: `
            background: white;
            padding: 20px;
            border-radius: 12px;
            max-width: 400px;
            width: 90%;
            max-height: 80vh;
            overflow-y: auto;
        `,
        accountItem: `
            padding: 12px;
            margin: 8px 0;
            border: 1px solid #e2e8f0;
            border-radius: 8px;
            cursor: pointer;
            transition: all 0.2s ease;
        `
    };

    const ESSENTIAL_COOKIES = ['epian-token', 'epian-user-id'];

    const MediaStore = {
        items: new Map(),

        clear() {
            this.items.clear();
        },

        add(type, url, posterUrl = null) {
            const processedUrl = type === '图片' ? reformatImageUrl(url) : url;
            if (this.items.has(processedUrl)) return false;

            this.items.set(processedUrl, {
                type,
                url: processedUrl,
                posterUrl,
                bbcode: this.generateBBCode(type, processedUrl, posterUrl)
            });
            return true;
        },

        generateBBCode(type, url, posterUrl = null) {
            switch(type) {
                case '图片':
                    return `[img]${url}[/img]`;
                case '音频':
                    return `[audio]${url}[/audio]`;
                case '视频':
                    return `[movie]${url}[/movie]`; // 基础BBCode不包含封面
            }
        },

        getAllUrls() {
            return Array.from(this.items.keys());
        },

        getAllBBCode() {
            return Array.from(this.items.values()).map(item => {
                // 普通BBCode永远不包含封面
                return this.generateBBCode(item.type, item.url);
            });
        },

        getAllBBCode2() {
            return Array.from(this.items.values()).map(item => {
                // BBCode2 仅在视频类型且有封面时才包含封面
                if (item.type === '视频' && item.posterUrl) {
                    return `[movie]${item.url}|${item.posterUrl}[/movie]`;
                }
                return this.generateBBCode(item.type, item.url);
            });
        },

        getItem(url) {
            return this.items.get(url);
        },

        size() {
            return this.items.size;
        }
    };

    function isValidUrl(url) {
        return url && (
            url.startsWith('https://media-volc.jianpian.info/') ||
            url.startsWith('https://img-volc.jianpian.info/')
        );
    }

    function reformatImageUrl(url) {
        if (!url.startsWith('https://img-volc.jianpian.info/')) return url;
        const match = url.match(/https:\/\/img-volc\.jianpian\.info\/([^?~]+)/);
        if (!match) return url;

        const filePath = match[1];
        if (filePath.includes('__transed__')) {
            const [basePath, extension] = filePath.split('__transed__');
            return `https://img-volc.jianpian.info/${basePath}.${extension.split('.')[0]}`;
        }
        return url;
    }

    function updateButtonState(button, originalText, duration = 1000) {
        button.textContent = '已复制';
        setTimeout(() => button.textContent = originalText, duration);
    }

    const MEDIA_STYLES = {
        container: `
            margin: 0 0 15px 0;
            padding: 12px;
            background: #f8f9fa;
            border-radius: 8px;
            border: 1px solid #e2e8f0;
        `,
        typeLabel: (type) => `
            display: inline-block;
            padding: 2px 8px;
            background: ${
                type === '视频' ? '#3182ce' : 
                type === '音频' ? '#38a169' : 
                '#805ad5'
            };
            color: white;
            border-radius: 4px;
            font-size: 12px;
            margin-bottom: 8px;
        `,
        url: `
            font-size: 14px;
            color: #4a5568;
            word-break: break-all;
            margin-bottom: 12px;
            line-height: 1.5;
            font-family: monospace;
        `,
        buttonGroup: `
            display: flex;
            gap: 10px;
        `
    };

    function addMediaLink(url, type, posterUrl = null) {
        if (!MediaStore.add(type, url, posterUrl)) return;

        const container = document.getElementById('mediaContent');
        const linkDiv = document.createElement('div');
        const mediaItem = MediaStore.getItem(type === '图片' ? reformatImageUrl(url) : url);
        
        linkDiv.style.cssText = MEDIA_STYLES.container;
        linkDiv.innerHTML = `
            <div style="${MEDIA_STYLES.typeLabel(type)}">${type}</div>
            <div style="${MEDIA_STYLES.url}">${mediaItem.url}</div>
            <div style="${MEDIA_STYLES.buttonGroup}">
                <button class="copy-btn" style="${STYLES.button}">复制链接</button>
                <button class="copy-bbcode-btn" style="${STYLES.button} background: #38a169 !important;">复制BBCode</button>
                ${type === '视频' && posterUrl ? `
                    <button class="copy-bbcode2-btn" style="${STYLES.button} background: #805ad5 !important;">复制BBCode2</button>
                ` : ''}
            </div>
        `;

        const copyBtn = linkDiv.querySelector('.copy-btn');
        const copyBBCodeBtn = linkDiv.querySelector('.copy-bbcode-btn');
        
        copyBtn.onclick = () => {
            navigator.clipboard.writeText(mediaItem.url);
            updateButtonState(copyBtn, '复制链接');
        };

        copyBBCodeBtn.onclick = () => {
            // 普通BBCode永远不包含封面
            navigator.clipboard.writeText(MediaStore.generateBBCode(type, mediaItem.url));
            updateButtonState(copyBBCodeBtn, '复制BBCode');
        };

        if (type === '视频' && posterUrl) {
            const copyBBCode2Btn = linkDiv.querySelector('.copy-bbcode2-btn');
            copyBBCode2Btn.onclick = () => {
                // BBCode2 包含封面
                navigator.clipboard.writeText(`[movie]${mediaItem.url}|${posterUrl}[/movie]`);
                updateButtonState(copyBBCode2Btn, '复制BBCode2');
            };
        }

        container.appendChild(linkDiv);
    }

    function createBatchCopyButtons() {
        const container = document.createElement('div');
        container.style.cssText = MEDIA_STYLES.buttonGroup;
        container.style.padding = '10px';
        container.style.marginBottom = '15px';

        const buttons = [
            ['复制全部链接', () => MediaStore.getAllUrls()],
            ['复制全部BBCode', () => MediaStore.getAllBBCode()],
            ['复制全部BBCode2', () => MediaStore.getAllBBCode2()]
        ];

        buttons.forEach(([text, getter]) => {
            const button = document.createElement('button');
            button.textContent = text;
            button.style.cssText = STYLES.button;
            button.onclick = () => {
                navigator.clipboard.writeText(getter().join('\n'));
                updateButtonState(button, text);
            };
            container.appendChild(button);
        });

        return container;
    }

    const POSTER_URL_REGEX = /url\("([^"]+)"\)/;

    const MediaScanner = {
        scanVideoPosters() {
            const posterMap = new Map();
            document.querySelectorAll('.poster').forEach(poster => {
                const style = poster.getAttribute('style');
                if (style) {
                    const match = style.match(POSTER_URL_REGEX);
                    if (match && isValidUrl(match[1])) {
                        const videoElem = poster.closest('div[class*="video"]')?.querySelector('video');
                        if (videoElem) {
                            const posterUrl = reformatImageUrl(match[1]);
                            posterMap.set(videoElem, posterUrl);
                        }
                    }
                }
            });
            return posterMap;
        },

        scan() {
            try {
                const posterMap = this.scanVideoPosters();

                // 扫描视频
                document.querySelectorAll('video').forEach(video => {
                    if (video.src && isValidUrl(video.src)) {
                        addMediaLink(video.src, '视频', posterMap.get(video));
                    }
                    video.querySelectorAll('source').forEach(source => {
                        if (source.src && isValidUrl(source.src)) {
                            addMediaLink(source.src, '视频', posterMap.get(video));
                        }
                    });
                });

                // 扫描音频
                document.querySelectorAll('audio').forEach(audio => {
                    if (audio.src && isValidUrl(audio.src)) {
                        addMediaLink(audio.src, '音频');
                    }
                    audio.querySelectorAll('source').forEach(source => {
                        if (source.src && isValidUrl(source.src)) {
                            addMediaLink(source.src, '音频');
                        }
                    });
                });

                // 扫描图片
                document.querySelectorAll('img').forEach(img => {
                    if (img.src && isValidUrl(img.src)) {
                        addMediaLink(img.src, '图片');
                    }
                });

                return MediaStore.size();
            } catch (error) {
                console.error('扫描媒体出错:', error);
                throw error;
            }
        }
    };

    function scanMedia() {
        const mediaContent = document.getElementById('mediaContent');
        if (!mediaContent || mediaContent.style.display === 'none') return;
        
        mediaContent.innerHTML = '';
        mediaContent.appendChild(createBatchCopyButtons());
        
        MediaStore.clear();
        
        requestAnimationFrame(() => {
            try {
                const count = MediaScanner.scan();
                if (count === 0) {
                    mediaContent.innerHTML = '<div style="text-align: center; padding: 30px;">未找到媒体文件</div>';
                }
            } catch (error) {
                mediaContent.innerHTML = '扫描媒体时出错,请刷新页面重试';
            }
        });
    }

    function getAccountList(accounts) {
        const accountNames = Object.keys(accounts);
        if (accountNames.length === 0) return null;
        
        return '已保存的账号:\n\n' + accountNames.map(name => {
            const account = accounts[name];
            return `${name}${account.note ? ` (${account.note})` : ''}\n保存时间: ${account.saveDate}`;
        }).join('\n\n');
    }

    function showAccountSelector(accounts, onSelect, title) {
        const modal = document.createElement('div');
        modal.style.cssText = STYLES.modal;
        
        const content = document.createElement('div');
        content.style.cssText = STYLES.modalContent;
        content.innerHTML = `<h3 style="margin: 0 0 16px 0; font-size: 18px;">${title}</h3>`;

        Object.entries(accounts).forEach(([name, account]) => {
            const item = document.createElement('div');
            item.style.cssText = STYLES.accountItem;
            item.innerHTML = `
                <div style="font-weight: 500; color: #2d3748;">${name}</div>
                ${account.note ? `<div style="color: #718096; font-size: 12px;">备注: ${account.note}</div>` : ''}
                <div style="color: #a0aec0; font-size: 12px;">保存时间: ${account.saveDate}</div>
            `;
            item.onmouseover = () => item.style.background = '#f7fafc';
            item.onmouseout = () => item.style.background = 'white';
            item.onclick = () => {
                onSelect(name);
                document.body.removeChild(modal);
            };
            content.appendChild(item);
        });

        const closeBtn = document.createElement('button');
        closeBtn.textContent = '取消';
        closeBtn.style.cssText = STYLES.button;
        closeBtn.onclick = () => document.body.removeChild(modal);
        content.appendChild(closeBtn);

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

        // 点击背景关闭
        modal.onclick = (e) => {
            if (e.target === modal) {
                document.body.removeChild(modal);
            }
        };
    }

    function checkAccounts() {
        const accounts = GM_getValue('accounts', {});
        if (Object.keys(accounts).length === 0) {
            alert('还没有保存任何账号!');
            return null;
        }
        return accounts;
    }

    function switchAccount() {
        const accounts = checkAccounts();
        if (!accounts) return;

        showAccountSelector(accounts, (selectedAccount) => {
            ESSENTIAL_COOKIES.forEach(name => {
                document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.jianpian.cn`;
            });

            accounts[selectedAccount].cookies.split(';').forEach(cookie => {
                document.cookie = `${cookie.trim()}; path=/; domain=.jianpian.cn`;
            });

            location.reload();
        }, '选择要切换的账号');
    }

    function deleteAccount() {
        const accounts = checkAccounts();
        if (!accounts) return;

        showAccountSelector(accounts, (selectedAccount) => {
            if (confirm(`确定要删除账号 "${selectedAccount}" 吗?`)) {
                delete accounts[selectedAccount];
                GM_setValue('accounts', accounts);
                alert('删除成功!');
            }
        }, '选择要删除的账号');
    }

    const BUTTON_CONFIGS = {
        saveCookie: ['保存当前账号', async () => {
            const currentCookies = document.cookie.split(';');
            const essentialCookies = currentCookies.filter(cookie => 
                ESSENTIAL_COOKIES.some(name => cookie.split('=')[0].trim() === name)
            );

            if (essentialCookies.length === 0) {
                alert('未检测到登录状态,请先登录!');
                return;
            }

            const accountName = prompt('请为当前账号设置一个名称:');
            if (!accountName) return;

            const accountNote = prompt('请输入账号备注(可):');
            const accounts = GM_getValue('accounts', {});
            accounts[accountName] = {
                cookies: essentialCookies.join(';'),
                note: accountNote || '',
                saveDate: new Date().toLocaleString()
            };
            GM_setValue('accounts', accounts);
            alert('保存成功!');
        }],
        switchAccount: ['切换账号', switchAccount],
        deleteAccount: ['删除账号', deleteAccount],
        exportAccounts: ['导出账号', () => {
            const accounts = GM_getValue('accounts', {});
            const blob = new Blob([JSON.stringify(accounts, null, 2)], { type: 'application/json' });
            const a = document.createElement('a');
            a.href = URL.createObjectURL(blob);
            a.download = '简篇账号数据.json';
            a.click();
            URL.revokeObjectURL(a.href);
        }],
        importAccounts: ['导入账号', () => {
            const input = document.createElement('input');
            input.type = 'file';
            input.accept = '.json';
            input.onchange = e => {
                const reader = new FileReader();
                reader.onload = event => {
                    try {
                        const accounts = JSON.parse(event.target.result);
                        GM_setValue('accounts', accounts);
                        alert('导入成功!');
                    } catch (error) {
                        alert('导入失败,文件格式错误!');
                    }
                };
                reader.readAsText(e.target.files[0]);
            };
            input.click();
        }]
    };

    function createUI() {
        const container = document.createElement('div');
        container.id = 'jianpianHelper';
        container.style.cssText = STYLES.floatingWindow;

        const minimizeBtn = document.createElement('div');
        minimizeBtn.textContent = '−';
        minimizeBtn.style.cssText = STYLES.minimizeButton;
        minimizeBtn.title = '最小化';

        let isMinimized = false;
        minimizeBtn.onclick = (e) => {
            e.stopPropagation();
            isMinimized = !isMinimized;
            
            if (isMinimized) {
                container.innerHTML = '简篇助手';
                container.style.cssText = STYLES.minimized;
                container.title = '点击展开';
                container.onclick = () => {
                    isMinimized = false;
                    container.style.cssText = STYLES.floatingWindow;
                    container.innerHTML = '';
                    setupUI();
                    setupAccountButtons();
                    container.onclick = null;
                };
            }
        };

        function setupUI() {
            const tabs = document.createElement('div');
            tabs.style.cssText = STYLES.tabs;
            
            const [accountTab, mediaTab] = ['账号管理', '媒体提取'].map(text => {
                const tab = document.createElement('div');
                tab.textContent = text;
                tab.style.cssText = STYLES.tab + (text === '账号管理' ? STYLES.activeTab : '');
                return tab;
            });

            tabs.append(accountTab, mediaTab);

            const [accountContent, mediaContent] = ['account', 'media'].map(id => {
                const content = document.createElement('div');
                content.id = `${id}Content`;
                content.style.cssText = STYLES.content + `; display: ${id === 'account' ? 'block' : 'none'};`;
                return content;
            });

            [accountTab, mediaTab].forEach((tab, i) => {
                tab.onclick = () => {
                    [accountTab, mediaTab].forEach((t, j) => 
                        t.style.cssText = STYLES.tab + (i === j ? STYLES.activeTab : '')
                    );
                    accountContent.style.display = i === 0 ? 'block' : 'none';
                    mediaContent.style.display = i === 0 ? 'none' : 'block';
                    if (i === 1) scanMedia();
                };
            });

            container.appendChild(minimizeBtn);
            container.appendChild(tabs);
            container.appendChild(accountContent);
            container.appendChild(mediaContent);
        }

        setupUI();
        return container;
    }

    function setupAccountButtons() {
        const accountContent = document.getElementById('accountContent');
        const accountButtons = document.createElement('div');
        accountButtons.style.cssText = `
            padding: 10px;
            display: grid;
            grid-template-columns: repeat(2, 1fr);
            gap: 10px;
        `;
        
        Object.entries(BUTTON_CONFIGS).forEach(([_, [text, handler]]) => {
            const button = document.createElement('button');
            button.textContent = text;
            button.style.cssText = STYLES.button;
            button.onclick = handler;
            accountButtons.appendChild(button);
        });

        accountContent.appendChild(accountButtons);
    }

    function init() {
        const container = createUI();
        document.body.appendChild(container);
        setupAccountButtons();
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();