豆瓣海报转存pixhost

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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();

})();