// ==UserScript== // @name eh // @namespace http://tampermonkey.net/ // @version 1.5 // @description 采集页面数据并发送到后端 // @author Jack // @match https://e-hentai.org/* // @grant GM_xmlhttpRequest // @grant GM_notification // ==/UserScript== (function() { 'use strict'; // 全局配置 const BACKEND_IP = '127.0.0.1'; const BACKEND_PORT = '55830'; const BUTTON_LOCATION_SELECTOR = '#gd5 > p:nth-child(5)'; const DATA_LIST_SELECTOR = '#gdt a'; const ALL_IMG_DATA = {}; const source = 'eh'; // 位置配置 - 只需调整这个数字即可同时调整按钮和进度条位置 const TOP_POSITION = 32; // 百分比位置,按钮在32%,进度条在36% // 并发配置 const CONCURRENT_LIMIT = 5; // 并发请求数量限制 const REQUEST_DELAY = 50; // 请求间延迟(毫秒) // 创建按钮 const button = document.createElement('button'); button.id = 'data-sender-button'; button.textContent = "send data"; button.style.position = "fixed"; button.style.top = `${TOP_POSITION}%`; button.style.right = "1%"; button.style.transform = "translateY(-50%)"; button.style.padding = "3px 8px"; button.style.fontSize = "10px"; button.style.backgroundColor = "#007baf"; button.style.color = "#fff"; button.style.border = "none"; button.style.borderRadius = "5px"; button.style.cursor = "pointer"; button.style.zIndex = "10000"; // 创建进度显示 - 位置在按钮下方4% const progressDiv = document.createElement('div'); progressDiv.id = 'progress-display'; progressDiv.style.position = "fixed"; progressDiv.style.top = `${TOP_POSITION + 4}%`; progressDiv.style.right = "1%"; progressDiv.style.padding = "5px 10px"; progressDiv.style.fontSize = "12px"; progressDiv.style.backgroundColor = "rgba(0,0,0,0.7)"; progressDiv.style.color = "#fff"; progressDiv.style.borderRadius = "5px"; progressDiv.style.zIndex = "9999"; progressDiv.style.display = "none"; document.body.appendChild(progressDiv); const targetElement = document.querySelector(BUTTON_LOCATION_SELECTOR); if (targetElement) { targetElement.appendChild(button); } else { document.body.appendChild(button); } function formatNumber(num) { return num.toString().padStart(4, '0'); } function getBaseUrl(url) { return url.replace(/([?&])p=\d+(&|$)/, (match, p1, p2) => { return p2 === '&' ? p1 : ''; }).replace(/[?&]$/, ''); } function buildPageUrl(baseUrl, page) { if (page === 0) { return baseUrl.includes('?') ? baseUrl : baseUrl; } else { const separator = baseUrl.includes('?') ? '&' : '?'; return baseUrl + separator + `p=${page}`; } } function extractThumbnailLinks(htmlContent) { const links = []; const tempDiv = document.createElement('div'); tempDiv.innerHTML = htmlContent; if (DATA_LIST_SELECTOR) { const linkElements = tempDiv.querySelectorAll(DATA_LIST_SELECTOR); linkElements.forEach(link => { const hrefAttr = link.getAttribute('href'); if (hrefAttr) { links.push(hrefAttr); } }); } return links; } function extractActualImageUrl(htmlContent) { const regex = /]*id="img"[^>]*src="([^"]*)"[^>]*>/i; const match = htmlContent.match(regex); return match ? match[1] : null; } async function fetchImagePage(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, headers: { "Referer": window.location.href, "Cookie": document.cookie }, onload: function(response) { resolve({url, html: response.responseText}); }, onerror: function(error) { reject({url, error}); } }); }); } // 并发处理函数 async function processThumbnailLinksConcurrently(thumbnailLinks, pageNum) { const results = []; const totalLinks = thumbnailLinks.length; // 更新进度显示 - 页数显示为 pageNum + 1(从1开始) const displayPageNum = pageNum + 1; updateProgress(`第${displayPageNum}页: 0/${totalLinks}`, 0); // 将链接分组,实现并发控制 for (let i = 0; i < thumbnailLinks.length; i += CONCURRENT_LIMIT) { const chunk = thumbnailLinks.slice(i, i + CONCURRENT_LIMIT); // 并发请求当前组 const promises = chunk.map(link => fetchImagePage(link)); try { const chunkResults = await Promise.all(promises); results.push(...chunkResults); // 更新进度 const processed = Math.min(i + CONCURRENT_LIMIT, totalLinks); updateProgress(`第${displayPageNum}页: ${processed}/${totalLinks}`, (processed / totalLinks) * 100); // 组间延迟,避免请求过快 if (i + CONCURRENT_LIMIT < thumbnailLinks.length) { await new Promise(resolve => setTimeout(resolve, REQUEST_DELAY)); } } catch (error) { console.error('并发请求组失败:', error); } } // 处理结果,提取真实图片URL const imageUrls = []; for (const result of results) { if (result.html) { const actualImageUrl = extractActualImageUrl(result.html); if (actualImageUrl) { imageUrls.push(actualImageUrl); } } } return imageUrls; } function updateProgress(text, percentage) { progressDiv.textContent = text; progressDiv.style.display = "block"; // 可以添加进度条样式 progressDiv.style.background = `linear-gradient(90deg, #007baf ${percentage}%, rgba(0,0,0,0.7) ${percentage}%)`; } async function processPage(page) { const baseUrl = getBaseUrl(window.location.href); const pageUrl = buildPageUrl(baseUrl, page); try { let htmlContent; if (page === 0) { htmlContent = document.documentElement.innerHTML; } else { htmlContent = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: pageUrl, headers: { "Referer": window.location.href, "Cookie": document.cookie }, onload: function(response) { resolve(response.responseText); }, onerror: function(error) { reject(error); } }); }); } const thumbnailLinks = extractThumbnailLinks(htmlContent); if (thumbnailLinks.length === 0) { console.log(`第${page + 1}页没有缩略图链接,可能是最后一页`); return false; } console.log(`第${page + 1}页找到${thumbnailLinks.length}个缩略图链接,开始并发获取真实图片URL...`); // 使用并发处理 const actualImageUrls = await processThumbnailLinksConcurrently(thumbnailLinks, page); let hasNewImage = false; actualImageUrls.forEach(url => { const isDuplicate = Object.values(ALL_IMG_DATA).includes(url); if (!isDuplicate) { ALL_IMG_DATA[formatNumber(Object.keys(ALL_IMG_DATA).length + 1)] = url; hasNewImage = true; } }); console.log(`第${page + 1}页采集完成,获取到${actualImageUrls.length}个真实图片链接`); return hasNewImage; } catch (error) { console.error(`第${page + 1}页采集失败:`, error); return false; } } function sendDataToBackend(data) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url: `http://${BACKEND_IP}:${BACKEND_PORT}/api/save_json`, headers: { "Content-Type": "application/json", }, data: JSON.stringify(data), onload: function(response) { if (response.status === 200) { try { const result = JSON.parse(response.responseText); if (result.success) { resolve(result); } else { reject(new Error(result.message || '后端保存失败')); } } catch (e) { reject(new Error('解析响应失败: ' + e.message)); } } else { reject(new Error(`后端返回错误: ${response.status} - ${response.responseText}`)); } }, onerror: function(error) { reject(error); } }); }); } button.addEventListener('click', async function() { const currentUrl = window.location.href; const pageTitle = document.title; Object.keys(ALL_IMG_DATA).forEach(key => delete ALL_IMG_DATA[key]); button.textContent = "采集中..."; button.disabled = true; progressDiv.style.display = "block"; try { let shouldContinue = true; let page = 0; let totalPagesProcessed = 0; while (shouldContinue && page <= 100) { // 显示页数为 page + 1(从1开始) updateProgress(`正在处理第${page + 1}页...`, (page / 100) * 50); const hasNewImages = await processPage(page); totalPagesProcessed++; if (!hasNewImages && page > 0) { console.log(`第${page + 1}页没有新图片,停止采集`); shouldContinue = false; } if (Object.keys(ALL_IMG_DATA).length >= 2200) { console.log('图片数量达到上限2200,停止采集'); shouldContinue = false; } page++; // 页面间延迟,避免请求过快 if (shouldContinue) { await new Promise(resolve => setTimeout(resolve, 200)); } } updateProgress(`处理完成,准备发送数据...`, 80); const data = { url: currentUrl, title: pageTitle, source: source, imgs: ALL_IMG_DATA, totalImages: Object.keys(ALL_IMG_DATA).length }; console.log('采集完成的所有数据:', data); // 发送数据到后端 const response = await sendDataToBackend(data); updateProgress(`数据发送成功!页面将在1秒后关闭...`, 100); // 显示成功通知 if (typeof GM_notification !== 'undefined') { GM_notification({ title: '数据采集完成', text: `已采集 ${Object.keys(ALL_IMG_DATA).length} 张图片,页面即将关闭`, timeout: 1000 }); } // 1秒后自动关闭页面,无需用户确认 setTimeout(() => { console.log(`数据保存成功,关闭页面。采集统计: - 标题: ${pageTitle} - 总图片数: ${Object.keys(ALL_IMG_DATA).length} - 处理页数: ${totalPagesProcessed} - 保存路径: ${response.folder || '未知'}`); window.close(); }, 1000); } catch (error) { console.error('采集失败:', error); updateProgress(`采集失败: ${error.message}`, 0); // 失败时显示错误信息,但也不弹框,只在控制台显示 console.error('数据采集失败!错误:', error.message); // 在进度条上显示错误信息 progressDiv.textContent = `采集失败: ${error.message}`; progressDiv.style.backgroundColor = "rgba(255,0,0,0.7)"; } finally { button.textContent = "send data"; button.disabled = false; // 如果发生错误,10秒后隐藏进度显示 setTimeout(() => { progressDiv.style.display = "none"; progressDiv.style.backgroundColor = "rgba(0,0,0,0.7)"; }, 10000); } }); })();