您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在e-hentai或exhentai中, 导出/入收藏
// ==UserScript== // @name EH收藏导出/入 // @namespace https://www.mohuangdiyu.com/ // @version 1.0 // @description 在e-hentai或exhentai中, 导出/入收藏 // @author 地狱 魔皇 // @match *://e-hentai.org/* // @match *://exhentai.org/* // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @connect e-hentai.org // @connect exhentai.org // @license MIT // ==/UserScript== (function () { 'use strict'; const validSites = ['e-hentai.org', 'exhentai.org']; if (!validSites.some(site => location.hostname.includes(site))) { alert('此脚本仅支持 e-hentai 和 exhentai'); return; } const styles = ` .eh-mask { position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 9998; } .eh-popup { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #fff; color: #111; padding: 20px; z-index: 9999; border-radius: 10px; min-width: 300px; max-height: 80vh; overflow: auto; box-shadow: 0 0 15px rgba(0,0,0,0.6); font-family: sans-serif; } .eh-popup h3 { margin-top: 0; color: #222; } .eh-popup select, .eh-popup button, .eh-popup input, .eh-popup label { width: 100%; margin-top: 10px; font-size: 14px; color: #000; background-color: #fff; } .eh-popup button { background-color: #007bff; color: #fff; border: none; padding: 8px; border-radius: 4px; cursor: pointer; } .eh-popup button:hover { background-color: #0056b3; } .eh-status { margin-top: 15px; font-size: 13px; max-height: 200px; overflow-y: auto; white-space: pre-wrap; background: #f9f9f9; color: #000; padding: 10px; border-radius: 5px; border: 1px solid #ccc; } `; function injectStyles() { const styleTag = document.createElement('style'); styleTag.textContent = styles; document.head.appendChild(styleTag); } function fetchFavoriteCategories(callback) { const base = location.origin; GM_xmlhttpRequest({ method: 'GET', url: base + '/favorites.php', onload: function (res) { const parser = new DOMParser(); const doc = parser.parseFromString(res.responseText, 'text/html'); const results = [ { name: 'All', id: 'all' } ]; doc.querySelectorAll('div.fp').forEach(div => { const onclick = div.getAttribute('onclick'); if (onclick && onclick.includes('favorites.php?favcat=')) { const match = onclick.match(/favcat=(\d+)/); const icon = div.querySelector('.i'); const name = icon ? icon.getAttribute('title') : null; if (match && name) { results.push({ id: match[1], name }); } } }); callback(results); }, onerror: function () { alert('无法加载收藏夹列表'); } }); } function fetchAllPages(startUrl, callback, accumulated = []) { GM_xmlhttpRequest({ method: 'GET', url: startUrl, onload: function (res) { const parser = new DOMParser(); const doc = parser.parseFromString(res.responseText, 'text/html'); const rows = doc.querySelectorAll('table.itg > tbody > tr'); rows.forEach(tr => { const a = tr.querySelector('td.gl3c.glname > a'); if (!a) return; const url = a.href; const title = a.querySelector('div.glink')?.textContent.trim() || ''; let note = a.querySelector('div.glfnote')?.textContent.trim() || ''; note = note.replace(/^Note:\s*/i, ''); // 查找以 posted_ 开头的 div const postedDiv = tr.querySelector('div[id^="posted_"]'); const favName = postedDiv?.getAttribute('title') || ''; const imgElement = tr.querySelector('div.glthumb img'); const img_url = imgElement ? (imgElement.getAttribute('data-src') || imgElement.getAttribute('src') || '').replace('s.exhentai.org', 'ehgt.org') : ''; accumulated.push({ 标题: title, 链接: url, 说明: note, 收藏夹: favName, 缩略图: img_url }); }); const next = doc.querySelector('a#unext'); if (next) { fetchAllPages(next.href, callback, accumulated); } else { callback(accumulated); } }, onerror: function () { alert('无法加载收藏内容'); } }); } function fetchFavorites(favcat, callback) { const base = location.origin; const url = favcat === 'all' ? base + '/favorites.php?inline_set=dm_l' : `${base}/favorites.php?inline_set=dm_l&favcat=${favcat}`; fetchAllPages(url, callback); } function downloadExportedData(data, format) { if (format === 'json') { const json = data.map(item => ({ title: item['标题'], url: item['链接'], note: item['说明'], favname: item['收藏夹'], img_url: item['缩略图'] })); const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); triggerDownload(url, 'favorites.json'); } else if (format === 'xlsx') { if (typeof XLSX === 'undefined') { alert('XLSX 库未加载'); return; } const worksheet = XLSX.utils.json_to_sheet(data); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, '收藏'); const wbout = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' }); const blob = new Blob([wbout], { type: 'application/octet-stream' }); const url = URL.createObjectURL(blob); triggerDownload(url, 'favorites.xlsx'); } } function triggerDownload(url, filename) { const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } function showExportPopup() { if (document.querySelector('.eh-mask')) return; fetchFavoriteCategories(favCats => { const mask = document.createElement('div'); mask.className = 'eh-mask'; const popup = document.createElement('div'); popup.className = 'eh-popup'; const title = document.createElement('h3'); title.textContent = '导出收藏'; const formatSelect = document.createElement('select'); ['json', 'xlsx'].forEach(fmt => { const opt = document.createElement('option'); opt.value = fmt; opt.textContent = `导出格式:${fmt}`; formatSelect.appendChild(opt); }); const favSelect = document.createElement('select'); favCats.forEach(cat => { const opt = document.createElement('option'); opt.value = cat.id; opt.textContent = `收藏夹:${cat.name}`; favSelect.appendChild(opt); }); const exportBtn = document.createElement('button'); exportBtn.textContent = '导出'; exportBtn.onclick = () => { const format = formatSelect.value; const favcat = favSelect.value; fetchFavorites(favcat, data => { downloadExportedData(data, format); }); closePopup(); }; popup.appendChild(title); popup.appendChild(formatSelect); popup.appendChild(favSelect); popup.appendChild(exportBtn); document.body.appendChild(mask); document.body.appendChild(popup); function closePopup() { mask.remove(); popup.remove(); } mask.addEventListener('click', closePopup); }); } function parseGalleryInfo(url) { const match = url.match(/\/g\/(\d+)\/([a-z0-9]+)\//); return match ? { gid: match[1], token: match[2] } : null; } function importFavoritesFromData(data, favcat, skipDuplicate, statusBox) { const base = location.origin; let cancelImport = false; // 添加取消按钮 const cancelBtn = document.createElement('button'); cancelBtn.textContent = '取消导入'; cancelBtn.style.marginTop = '10px'; cancelBtn.onclick = () => cancelImport = true; statusBox.parentElement.appendChild(cancelBtn); function extractGids(existingGids) { const gidSet = new Set(); // 遍历existingGids数组 for (let i = 0; i < existingGids.length; i++) { const url = existingGids[i]['链接']; const info = parseGalleryInfo(url); if (info && info.gid) { gidSet.add(info.gid); } } return gidSet; } function doImport(existingGids) { let success = 0, fail = 0; const importOne = (item, index) => { if (cancelImport) { statusBox.textContent += `\n🚫 导入已取消\n`; return; } const info = parseGalleryInfo(item.url); if (!info) { statusBox.textContent += `❌ 无效链接: ${item.url}\n`; fail++; return; } if (skipDuplicate && existingGids.has(info.gid)) { statusBox.textContent += `⚠️ 已存在跳过: ${item.url}\n`; return; } const formData = new URLSearchParams(); formData.set('favcat', favcat); formData.set('favnote', item.note || ''); formData.set('apply', 'Add to Favorites'); formData.set('update', '1'); GM_xmlhttpRequest({ method: 'POST', url: `${base}/gallerypopups.php?gid=${info.gid}&t=${info.token}&act=addfav`, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: formData.toString(), onload: () => { if (cancelImport) return; statusBox.textContent += `✅ 成功: ${item.url}\n`; success++; }, onerror: () => { if (cancelImport) return; statusBox.textContent += `❌ 失败: ${item.url}\n`; fail++; } }); }; for (let i = 0; i < data.length; i++) { if (cancelImport) break; importOne(data[i], i); } } if (skipDuplicate) { fetchAllPages(base + '/favorites.php', (existingGids) => { const gidSet = extractGids(existingGids); doImport(gidSet); }); } else { doImport(); } } function showImportPopup() { if (document.querySelector('.eh-mask')) return; fetchFavoriteCategories(favCats => { const mask = document.createElement('div'); mask.className = 'eh-mask'; const popup = document.createElement('div'); popup.className = 'eh-popup'; const title = document.createElement('h3'); title.textContent = '导入收藏'; const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.json,.xlsx'; const favSelect = document.createElement('select'); favCats.forEach(cat => { if (cat.name === "All") return; const opt = document.createElement('option'); opt.value = cat.id; opt.textContent = `收藏夹:${cat.name}`; favSelect.appendChild(opt); }); const skipCheckbox = document.createElement('input'); skipCheckbox.type = 'checkbox'; skipCheckbox.id = 'skipDup'; const skipLabel = document.createElement('label'); skipLabel.textContent = '跳过已存在的收藏'; skipLabel.htmlFor = 'skipDup'; const importBtn = document.createElement('button'); importBtn.textContent = '开始导入'; const statusBox = document.createElement('div'); statusBox.className = 'eh-status'; statusBox.textContent = '等待导入...'; const controlBox = document.createElement('div'); const checkboxMap = []; // 用于收集所有复选框状态 importBtn.onclick = () => { const file = fileInput.files[0]; if (!file) return alert('请先选择文件'); const reader = new FileReader(); reader.onload = function () { let data; if (file.name.endsWith('.json')) { try { data = JSON.parse(reader.result); } catch (e) { return alert('JSON格式错误'); } } else if (file.name.endsWith('.xlsx')) { if (typeof XLSX === 'undefined') return alert('XLSX 库未加载'); const workbook = XLSX.read(reader.result, { type: 'binary' }); const sheet = workbook.Sheets[workbook.SheetNames[0]]; data = XLSX.utils.sheet_to_json(sheet); } if (!Array.isArray(data)) return alert('导入数据无效'); // 清空旧的状态 statusBox.textContent = ''; statusBox.style.display = 'flex'; statusBox.style.flexWrap = 'wrap'; statusBox.style.gap = '12px'; statusBox.style.height = '50vh'; statusBox.style.overflowY = 'auto'; controlBox.style.marginBottom = '10px'; controlBox.style.display = 'flex'; controlBox.style.justifyContent = 'space-between'; controlBox.style.alignItems = 'center'; const counter = document.createElement('span'); counter.textContent = `已勾选 0 / ${data.length}`; const selectAllBtn = document.createElement('button'); selectAllBtn.textContent = '全选'; selectAllBtn.onclick = () => { checkboxMap.forEach(checkbox => checkbox.checked = true); updateCounter(); }; const invertBtn = document.createElement('button'); invertBtn.textContent = '反选'; invertBtn.onclick = () => { checkboxMap.forEach(checkbox => checkbox.checked = !checkbox.checked); updateCounter(); }; function updateCounter() { const checked = checkboxMap.filter(cb => cb.checked).length; counter.textContent = `已勾选 ${checked} / ${data.length}`; } const buttonGroup = document.createElement('div'); buttonGroup.appendChild(selectAllBtn); buttonGroup.appendChild(invertBtn); controlBox.appendChild(counter); controlBox.appendChild(buttonGroup); // 构建列表 data.forEach((item, index) => { const label = document.createElement('label'); label.style.display = 'flex'; label.style.alignItems = 'center'; label.style.flex = '1 1 calc(33.333% - 12px)'; label.style.padding = '10px'; label.style.marginBottom = '10px'; label.style.border = '1px solid #ccc'; label.style.borderRadius = '6px'; label.style.background = '#f4f4f4'; label.style.boxSizing = 'border-box'; label.style.gap = '8px'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = true; checkboxMap.push(checkbox); checkbox.onclick = updateCounter; const title = item.title || item.标题 || '无标题'; const note = item.note || item.说明 || ''; const url = item.url || item.链接 || ''; const favName = item.收藏夹 || item.favname; const thumbUrl = item.img_url || item.缩略图 const img = document.createElement('img'); img.src = thumbUrl; img.style.width = '60px'; img.style.height = 'auto'; img.style.flexShrink = '0'; img.style.borderRadius = '4px'; img.style.objectFit = 'cover'; const span = document.createElement('span'); span.textContent = title; span.style.marginLeft = '8px'; span.title = `链接: ${url}\n说明: ${note}\n收藏夹: ${favName}`; label.appendChild(checkbox); label.appendChild(img) label.appendChild(span); statusBox.appendChild(label); }); // 替换“开始导入”按钮行为 importBtn.textContent = '确认导入已勾选项'; importBtn.onclick = () => { const selectedData = data.filter((_, i) => checkboxMap[i].checked); if (selectedData.length === 0) return alert('请至少勾选一个收藏'); statusBox.textContent = `正在导入 ${selectedData.length} 个收藏...\n`; importFavoritesFromData(selectedData, favSelect.value, skipCheckbox.checked, statusBox); }; }; if (file.name.endsWith('.xlsx')) reader.readAsBinaryString(file); else reader.readAsText(file); }; popup.appendChild(title); popup.appendChild(fileInput); popup.appendChild(favSelect); popup.appendChild(skipCheckbox); popup.appendChild(skipLabel); popup.appendChild(importBtn); popup.appendChild(statusBox); popup.appendChild(controlBox); document.body.appendChild(mask); document.body.appendChild(popup); function closePopup() { mask.remove(); popup.remove(); } mask.addEventListener('click', closePopup); }); } function load() { GM_registerMenuCommand('📤 导出收藏', showExportPopup); GM_registerMenuCommand('📥 导入收藏', showImportPopup); // 自动加载 xlsx 库 const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js'; document.head.appendChild(script); } injectStyles(); load(); })();