豆瓣海报转存pixhost

从豆瓣页面提取高清海报并上传到Pixhost,支持多CDN域名轮询下载

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         豆瓣海报转存pixhost
// @namespace    http://tampermonkey.net/
// @version      1.0.2
// @description  从豆瓣页面提取高清海报并上传到Pixhost,支持多CDN域名轮询下载
// @author       guyuanwind
// @match        https://movie.douban.com/subject/*
// @match        https://book.douban.com/subject/*
// @match        https://music.douban.com/subject/*
// @grant        GM_xmlhttpRequest
// @grant        GM_log
// @connect      img1.doubanio.com
// @connect      img2.doubanio.com
// @connect      img3.doubanio.com
// @connect      img4.doubanio.com
// @connect      img5.doubanio.com
// @connect      img6.doubanio.com
// @connect      img7.doubanio.com
// @connect      img8.doubanio.com
// @connect      img9.doubanio.com
// @connect      api.pixhost.to
// @connect      dou.img.lithub.cc
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js
// @license      GPL-3.0
// ==/UserScript==

(function() {
    'use strict';

    // 从URL中提取豆瓣ID
    function getDoubanId() {
        const match = window.location.href.match(/subject\/(\d+)/);
        return match ? match[1] : null;
    }

    // 豆瓣海报提取函数(多域名轮询版)
    function getDoubanPoster() {
        try {
            const posterImg = document.querySelector('#mainpic img');
            if (!posterImg) {
                throw new Error('未找到海报图片');
            }
            
            const originalSrc = posterImg.src;
            console.log('原始图片URL:', originalSrc);
            
            // 提取域名数字和图片ID
            const domainMatch = originalSrc.match(/https:\/\/img(\d+)\.doubanio\.com/);
            const imageIdMatch = originalSrc.match(/(p\d+)/);
            
            if (!domainMatch || !imageIdMatch) {
                throw new Error('无法解析图片URL格式');
            }
            
            const originalDomainNumber = domainMatch[1]; // 提取数字(如 "2")
            const imageId = imageIdMatch[1];
            
            // 生成候选URL:优先原始域名,然后尝试 img1-img9
            const candidates = [];
            const domainNumbers = [originalDomainNumber]; // 原始域名优先
            
            // 添加其他域名(1-9,排除原始域名)
            for (let i = 1; i <= 9; i++) {
                if (i.toString() !== originalDomainNumber) {
                    domainNumbers.push(i.toString());
                }
            }
            
            // 路径优先级:先高清,后中清(只尝试jpg格式)
            const paths = [
                'view/photo/l_ratio_poster/public',
                'view/photo/m_ratio_poster/public'
            ];
            
            // 生成候选URL矩阵:9个域名 × 2个路径 = 18个候选URL
            domainNumbers.forEach(num => {
                paths.forEach(path => {
                    candidates.push(`https://img${num}.doubanio.com/${path}/${imageId}.jpg`);
                });
            });
            
            console.log(`生成 ${candidates.length} 个候选URL,原始域名: img${originalDomainNumber}`);
            
            return {
                original: originalSrc,
                candidates: candidates,
                imageId: imageId,
                originalDomain: `img${originalDomainNumber}.doubanio.com`
            };
        } catch (e) {
            throw new Error('海报提取失败: ' + e.message);
        }
    }

    // 第三方托管海报URL构造
    function getThirdPartyPosterUrl() {
        const doubanId = getDoubanId();
        if (!doubanId) {
            throw new Error('无法从页面URL提取豆瓣ID');
        }
        return `https://dou.img.lithub.cc/movie/${doubanId}.jpg`;
    }

    // 验证图片URL是否可访问
    function validateImageUrl(imageUrl) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'HEAD',
                url: imageUrl,
                headers: {
                    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
                    'Referer': 'https://movie.douban.com/'
                },
                onload: function(response) {
                    if (response.status === 200) {
                        const contentType = response.responseHeaders.toLowerCase();
                        const contentLength = response.responseHeaders.match(/content-length:\s*(\d+)/i);
                        const fileSize = contentLength ? parseInt(contentLength[1]) : 0;
                        
                        if (contentType.includes('image/') && fileSize > 1024) {
                            resolve({
                                url: imageUrl,
                                size: fileSize,
                                valid: true
                            });
                        } else {
                            reject(new Error('无效的图片文件'));
                        }
                    } else {
                        reject(new Error(`HTTP ${response.status}`));
                    }
                },
                onerror: function(error) {
                    reject(new Error('URL验证失败'));
                },
                timeout: 10000
            });
        });
    }

    // 将图片URL转换为Blob对象
    function urlToBlob(imageUrl) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: imageUrl,
                responseType: 'blob',
                headers: {
                    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
                    'Referer': 'https://movie.douban.com/'
                },
                onload: function(response) {
                    try {
                        if (response.status !== 200) {
                            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
                        }
                        
                        const blob = response.response;
                        
                        // 检查文件大小
                        if (blob.size === 0) {
                            throw new Error('下载的图片文件为空');
                        }
                        
                        if (blob.size > 10 * 1024 * 1024) {
                            throw new Error('图片文件过大 (>10MB)');
                        }
                        
                        resolve(blob);
                    } catch (e) {
                        reject(new Error('图片处理失败: ' + e.message));
                    }
                },
                onerror: function(error) {
                    reject(new Error('图片下载失败: ' + (error.statusText || '网络错误')));
                },
                ontimeout: function() {
                    reject(new Error('图片下载超时'));
                },
                timeout: 30000 // 30秒超时
            });
        });
    }

    // 上传到Pixhost(基于PixhostUpload.sh逻辑)
    function uploadToPixhost(blob, filename = 'poster.jpg') {
        return new Promise((resolve, reject) => {
            const formData = new FormData();
            formData.append('img', blob, filename);
            formData.append('content_type', '0');
            formData.append('max_th_size', '420');

            GM_xmlhttpRequest({
                method: 'POST',
                url: 'https://api.pixhost.to/images',
                headers: {
                    'Accept': 'application/json'
                },
                data: formData,
                onload: function(response) {
                    try {
                        const data = JSON.parse(response.responseText);
                        
                        if (!data.show_url) {
                            reject('API未返回有效URL');
                            return;
                        }
                        
                        // 转换为直链URL(基于PixhostUpload.sh的转换逻辑)
                        const directUrl = convertToDirectUrl(data.show_url);
                        if (!directUrl) {
                            reject('URL转换失败');
                            return;
                        }
                        
                        resolve({
                            showUrl: data.show_url,
                            directUrl: directUrl,
                            bbCode: `[img]${directUrl}[/img]`
                        });
                    } catch (e) {
                        reject('解析响应失败: ' + e.message);
                    }
                },
                onerror: function(error) {
                    reject('上传请求失败: ' + error.statusText);
                },
                ontimeout: function() {
                    reject('上传超时');
                },
                timeout: 30000 // 30秒超时
            });
        });
    }

    // URL转换函数(基于PixhostUpload.sh逻辑)
    function convertToDirectUrl(showUrl) {
        try {
            // 方案1: 直接替换域名和路径
            let directUrl = showUrl
                .replace(/https:\/\/pixhost\.to\/show\//, 'https://img1.pixhost.to/images/')
                .replace(/https:\/\/pixhost\.to\/th\//, 'https://img1.pixhost.to/images/')
                .replace(/_..\.jpg$/, '.jpg');

            // 方案2: 正则提取重建URL
            if (!directUrl.startsWith('https://img1.pixhost.to/images/')) {
                const match = showUrl.match(/(\d+)\/([^\/]+\.(jpg|png|gif))/);
                if (match) {
                    directUrl = `https://img1.pixhost.to/images/${match[1]}/${match[2]}`;
                }
            }

            // 最终验证
            if (/^https:\/\/img1\.pixhost\.to\/images\/\d+\/[^\/]+\.(jpg|png|gif)$/.test(directUrl)) {
                return directUrl;
            } else {
                console.error('URL转换失败:', showUrl);
                return null;
            }
        } catch (e) {
            console.error('URL转换异常:', e);
            return null;
        }
    }

    // 创建结果显示弹窗
    function showResult(result, posterUrl, source = 'unknown', sourceInfo = '') {
        const modal = document.createElement('div');
        modal.id = 'poster-upload-result';
        modal.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.7);
            z-index: 10000;
            display: flex;
            justify-content: center;
            align-items: center;
        `;

        // 复制到剪贴板的通用函数
        const copyToClipboard = (text, type) => {
            navigator.clipboard.writeText(text).then(() => {
                // 创建简洁的提示
                const toast = document.createElement('div');
                toast.textContent = `${type}已复制到剪贴板`;
                toast.style.cssText = `
                    position: fixed;
                    top: 20px;
                    right: 20px;
                    background: #4CAF50;
                    color: white;
                    padding: 10px 15px;
                    border-radius: 5px;
                    z-index: 10001;
                    font-size: 14px;
                    box-shadow: 0 2px 10px rgba(0,0,0,0.3);
                `;
                document.body.appendChild(toast);
                setTimeout(() => toast.remove(), 2000);
            }).catch(() => {
                alert('复制失败,请手动复制');
            });
        };

        modal.innerHTML = `
            <div id="modal-content" style="
                background: white;
                padding: 20px;
                border-radius: 12px;
                max-width: 600px;
                width: 85%;
                max-height: 70vh;
                overflow-y: auto;
                position: relative;
                box-shadow: 0 8px 32px rgba(0,0,0,0.3);
            ">
                <!-- 右上角固定关闭按钮 -->
                <button id="close-btn"
                        style="
                            position: fixed;
                            width: 32px;
                            height: 32px;
                            border: none;
                            background: #f5f5f5;
                            border-radius: 50%;
                            cursor: pointer;
                            font-size: 18px;
                            color: #666;
                            display: flex;
                            align-items: center;
                            justify-content: center;
                            transition: all 0.2s ease;
                            z-index: 10002;
                            box-shadow: 0 2px 8px rgba(0,0,0,0.2);
                        "
                        onmouseover="this.style.background='#e0e0e0'"
                        onmouseout="this.style.background='#f5f5f5'">×</button>

                <h3 style="margin: 0 30px 20px 0; text-align: center; color: #333; font-size: 18px;">海报上传成功 ✓</h3>
                
                ${sourceInfo ? `<div style="text-align: center; margin-bottom: 15px; padding: 8px; background: #e8f5e8; border-radius: 5px; border: 1px solid #4caf50;">
                    <small style="color: #2e7d32; font-weight: bold;">📸 ${sourceInfo}</small>
                </div>` : ''}
                
                <div style="margin-bottom: 15px;">
                    <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
                        <label style="font-weight: bold; color: #555; font-size: 14px;">原图链接 (${source}):</label>
                        <button id="copy-douban" data-text="${posterUrl}" data-type="原图链接"
                                style="padding: 4px 8px; background: #2196F3; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px;">复制</button>
                    </div>
                    <input readonly value="${posterUrl}" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-family: monospace; font-size: 12px; background: #f9f9f9;">
                </div>
                
                <div style="margin-bottom: 15px;">
                    <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
                        <label style="font-weight: bold; color: #555; font-size: 14px;">Pixhost 直链:</label>
                        <button id="copy-direct" data-text="${result.directUrl}" data-type="直链"
                                style="padding: 4px 8px; background: #FF9800; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px;">复制</button>
                    </div>
                    <input readonly value="${result.directUrl}" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-family: monospace; font-size: 12px; background: #f9f9f9;">
                </div>
                
                <div style="margin-bottom: 20px;">
                    <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
                        <label style="font-weight: bold; color: #555; font-size: 14px;">BBCode 代码:</label>
                        <button id="copy-bbcode" data-text="${result.bbCode}" data-type="BBCode"
                                style="padding: 4px 8px; background: #4CAF50; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px;">复制</button>
                    </div>
                    <input readonly value="${result.bbCode}" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-family: monospace; font-size: 12px; background: #f9f9f9;">
                </div>
                
                <div style="text-align: center; padding-top: 10px; border-top: 1px solid #eee;">
                    <small style="color: #666;">💡 点击对应的复制按钮快速复制链接</small>
                    ${source === 'third-party' ? '<br><small style="color: #ff9800;">⚠️ 使用了第三方图片托管服务</small>' : ''}
                </div>
            </div>
        `;

        document.body.appendChild(modal);

        // 动态定位关闭按钮
        const positionCloseButton = () => {
            const modalContent = document.getElementById('modal-content');
            const closeBtn = document.getElementById('close-btn');
            
            if (modalContent && closeBtn) {
                const rect = modalContent.getBoundingClientRect();
                closeBtn.style.top = (rect.top + 10) + 'px';
                closeBtn.style.left = (rect.right - 42) + 'px';
                closeBtn.style.right = 'auto'; // 清除right定位
            }
        };

        // 初始定位
        setTimeout(positionCloseButton, 10);

        // 窗口大小改变时重新定位
        const resizeHandler = () => positionCloseButton();
        window.addEventListener('resize', resizeHandler);

        // ESC键关闭处理函数
        const handleKeyPress = (e) => {
            if (e.key === 'Escape') {
                closeModal();
            }
        };

        // 统一的关闭函数
        const closeModal = () => {
            modal.remove();
            window.removeEventListener('resize', resizeHandler);
            document.removeEventListener('keydown', handleKeyPress);
        };

        // 关闭按钮事件监听器
        const closeBtn = modal.querySelector('#close-btn');
        if (closeBtn) {
            closeBtn.addEventListener('click', closeModal);
        }

        // 添加复制按钮事件监听器
        const copyButtons = ['copy-douban', 'copy-direct', 'copy-bbcode'];
        copyButtons.forEach(buttonId => {
            const button = modal.querySelector('#' + buttonId);
            if (button) {
                button.addEventListener('click', () => {
                    const text = button.getAttribute('data-text');
                    const type = button.getAttribute('data-type');
                    copyToClipboard(text, type);
                });
            }
        });

        // 添加事件监听器
        document.addEventListener('keydown', handleKeyPress);

        // 点击背景关闭
        modal.addEventListener('click', (e) => {
            if (e.target === modal) {
                closeModal();
            }
        });

    }

    // 智能海报获取函数(双重保障策略)
    async function getSmartPoster() {
        console.log('开始智能海报获取...');
        
        try {
            // 第一优先级:豆瓣官方高清图
            console.log('尝试豆瓣官方高清图...');
            const posterInfo = getDoubanPoster();
            
            // 依次尝试豆瓣候选URL(遍历 img1-img9 × 2个路径)
            for (let i = 0; i < posterInfo.candidates.length; i++) {
                const candidateUrl = posterInfo.candidates[i];
                
                // 提取当前尝试的域名和路径信息
                const domainInfo = candidateUrl.match(/img(\d+)\.doubanio\.com/);
                const pathInfo = candidateUrl.includes('l_ratio_poster') ? '高清' : '中清';
                const domainNum = domainInfo ? domainInfo[1] : '?';
                
                console.log(`测试 [${i + 1}/${posterInfo.candidates.length}] img${domainNum} (${pathInfo}):`, candidateUrl);
                
                try {
                    const validation = await validateImageUrl(candidateUrl);
                    console.log(`✓ 成功!使用 img${domainNum} 域名,文件大小: ${Math.round(validation.size / 1024)}KB`);
                    return {
                        url: candidateUrl,
                        source: 'douban',
                        quality: candidateUrl.includes('l_ratio_poster') ? 'high' : 'medium',
                        size: validation.size
                    };
                } catch (e) {
                    console.log(`✗ img${domainNum} 失败:`, e.message);
                    continue;
                }
            }
            
            // 第二优先级:第三方托管
            console.log('豆瓣官方图片全部失败,尝试第三方托管...');
            const thirdPartyUrl = getThirdPartyPosterUrl();
            console.log('测试第三方URL:', thirdPartyUrl);
            
            try {
                const validation = await validateImageUrl(thirdPartyUrl);
                console.log('✓ 第三方URL验证成功:', validation);
                return {
                    url: thirdPartyUrl,
                    source: 'third-party',
                    quality: 'high',
                    size: validation.size
                };
            } catch (e) {
                console.log('✗ 第三方URL失败:', e.message);
            }
            
            throw new Error('无法获取高质量海报图片,所有方案都失败了');
            
        } catch (e) {
            console.error('智能海报获取失败:', e);
            throw e;
        }
    }

    // 显示错误信息
    function showError(message) {
        alert('错误: ' + message);
        console.error('豆瓣海报上传错误:', message);
    }

    // 显示加载状态
    function showLoading(show = true, message = '正在处理...', detail = '提取海报并上传中,请稍候') {
        let loading = document.getElementById('poster-upload-loading');
        
        if (show) {
            if (!loading) {
                loading = document.createElement('div');
                loading.id = 'poster-upload-loading';
                loading.style.cssText = `
                    position: fixed;
                    top: 50%;
                    left: 50%;
                    transform: translate(-50%, -50%);
                    background: rgba(0, 0, 0, 0.85);
                    color: white;
                    padding: 25px;
                    border-radius: 12px;
                    z-index: 9999;
                    text-align: center;
                    min-width: 300px;
                    box-shadow: 0 4px 20px rgba(0,0,0,0.5);
                `;
                document.body.appendChild(loading);
            }
            
            loading.innerHTML = `
                <div style="font-size: 16px; margin-bottom: 10px;">
                    <span style="display: inline-block; width: 20px; height: 20px; border: 2px solid #fff; border-radius: 50%; border-top: 2px solid transparent; animation: spin 1s linear infinite; margin-right: 10px;"></span>
                    ${message}
                </div>
                <div style="font-size: 12px; color: #ccc;">${detail}</div>
                <style>
                    @keyframes spin {
                        0% { transform: rotate(0deg); }
                        100% { transform: rotate(360deg); }
                    }
                </style>
            `;
        } else if (!show && loading) {
            loading.remove();
        }
    }

    // 主处理函数
    async function handlePosterUpload() {
        try {
            console.log('开始豆瓣海报提取上传流程...');

            // 1. 智能海报获取
            showLoading(true, '步骤 1/3', '正在智能识别最佳海报来源...');
            console.log('步骤1: 智能海报获取...');
            const posterResult = await getSmartPoster();
            console.log('✓ 海报获取成功:', posterResult);
            
            // 根据来源显示不同的提示
            let sourceInfo = '';
            switch (posterResult.source) {
                case 'douban':
                    sourceInfo = posterResult.quality === 'high' ? '豆瓣高清海报' : '豆瓣中等清晰度海报';
                    break;
                case 'third-party':
                    sourceInfo = '第三方高质量海报';
                    break;
            }

            // 2. 下载图片
            showLoading(true, '步骤 2/3', `正在下载${sourceInfo}...`);
            console.log(`步骤2: 下载图片 [${posterResult.source}]...`);
            const blob = await urlToBlob(posterResult.url);
            console.log('✓ 图片下载成功, 大小:', Math.round(blob.size / 1024) + 'KB');

            // 3. 上传到Pixhost
            showLoading(true, '步骤 3/3', '正在上传到Pixhost图床...');
            console.log('步骤3: 上传到Pixhost...');
            const result = await uploadToPixhost(blob);
            console.log('✓ 上传成功:', result);

            showLoading(false);
            showResult(result, posterResult.url, posterResult.source, sourceInfo);
            console.log('豆瓣海报提取上传流程完成!');

        } catch (error) {
            showLoading(false);
            console.error('❌ 处理失败:', error);
            showError(error.message || error);
        }
    }

    // 创建上传按钮
    function createUploadButton() {
        // 检查是否已存在按钮
        if (document.getElementById('douban-poster-upload-btn')) {
            return;
        }

        const button = document.createElement('button');
        button.id = 'douban-poster-upload-btn';
        button.textContent = '提取并上传海报';
        button.style.cssText = `
            padding: 8px 15px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 13px;
            margin: 5px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            transition: all 0.3s ease;
        `;

        // 鼠标悬停效果
        button.addEventListener('mouseenter', () => {
            button.style.transform = 'translateY(-2px)';
            button.style.boxShadow = '0 4px 10px rgba(0,0,0,0.3)';
        });

        button.addEventListener('mouseleave', () => {
            button.style.transform = 'translateY(0)';
            button.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
        });

        button.addEventListener('click', handlePosterUpload);

        // 找个合适的位置插入按钮
        const mainpic = document.querySelector('#mainpic');
        if (mainpic) {
            mainpic.appendChild(button);
        } else {
            // 备选位置
            const info = document.querySelector('#info');
            if (info) {
                info.insertBefore(button, info.firstChild);
            }
        }
    }

    // 页面加载完成后初始化
    function init() {
        // 等待页面加载完成
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', init);
            return;
        }

        // 延迟创建按钮,确保页面元素加载完成
        setTimeout(createUploadButton, 1000);
    }

    // 启动脚本
    init();

})();