您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
获取e站所有收藏,以及对所有标签进行排序以找到你最爱的标签,可按namespace分组,支持翻译
// ==UserScript== // @name e站收藏统计 // @namespace Schwi // @version 1.9 // @description 获取e站所有收藏,以及对所有标签进行排序以找到你最爱的标签,可按namespace分组,支持翻译 // @author Schwi // @match *://e-hentai.org/* // @match *://exhentai.org/* // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/FileSaver.min.js // @icon https://e-hentai.org/favicon.ico // @grant GM_registerMenuCommand // @noframes // @license GPL-3.0 // ==/UserScript== (function () { 'use strict'; // 在 https://e-hentai.org/ 或 https://exhentai.org/ 任意页面运行即可 // 是否翻译标签(需下载翻译文本) const config = { translationUrl: "https://raw.githubusercontent.com/EhTagTranslation/DatabaseReleases/master/db.text.json", favoritesUrl: location.origin + "/favorites.php?inline_set=dm_e" } async function fetchFavorites(url) { try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.text(); } catch (error) { console.error('Fetch error:', error); } } async function getFavoritesList(queryUrl) { let favList = []; let nextUrl = queryUrl.href; while (nextUrl) { let resp = await fetchFavorites(nextUrl); if (resp) { let doc = new DOMParser().parseFromString(resp, "text/html"); let next = doc.scripts[3]; let scriptContent = next.textContent || next.innerText; let match = scriptContent.match(/var nexturl="(.*?)"/); nextUrl = match && match[1]; if (nextUrl && nextUrl.startsWith('http') && new URL(nextUrl).pathname != queryUrl.pathname) { nextUrl = null; } favList = favList.concat(Array.from(doc.querySelectorAll(".itg.glte>tbody>tr"))); } } return favList; } function parseFavorites(favList) { let myFavList = []; favList.forEach(fav => { let title = fav.querySelector(".glink").innerText; let url = fav.href; let reclass = fav.querySelector(".cn").innerText; let tags = []; fav.querySelectorAll("td>[title]").forEach(tag => { let title = tag.title; if (title.startsWith(":")) { title = "temp" + title; } tags.push(title); }); myFavList.push({ title, url, reclass, tags }); }); return myFavList; } async function getTranslate(translationUrl) { showProgress('正在获取翻译...'); try { const response = await fetch(translationUrl); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { console.error('Fetch error:', error); } } async function translateResult(myFavList, translate) { const reclassList = getReclassList(myFavList); const tagList = getTagList(myFavList); const groupedTagList = getGroupedTagList(myFavList); showProgress('正在翻译...'); reclassList.forEach(reclass => { if (reclass.reclass.toLowerCase().replace(' ', '') in translate.data[1].data) { reclass.translate = translate.data[1].data[reclass.reclass.toLowerCase().replace(' ', '')].name; reclass.intro = translate.data[1].data[reclass.reclass.toLowerCase().replace(' ', '')].intro || ''; } }); tagList.forEach(fullTag => { let namespace = fullTag.tag.split(":")[0]; let tag = fullTag.tag.split(":")[1]; let data = translate.data.filter(title => title.namespace === namespace); let tagTranslate = tag; let intro = ''; if (data.length > 0) { namespace = data[0].frontMatters.name; if (tag in data[0].data) { tagTranslate = data[0].data[tag].name; intro = data[0].data[tag].intro || ''; } } fullTag.translate = `${namespace}:${tagTranslate}`; fullTag.intro = intro; }); groupedTagList.forEach(group => { let data = translate.data.filter(title => title.namespace === group.namespace); if (data.length > 0) { group.translate = data[0].frontMatters.name; group.tags.forEach(tag => { tag.translate = tag.tag; if (tag.tag in data[0].data) { tag.translate = data[0].data[tag.tag].name; tag.intro = data[0].data[tag.tag].intro || ''; } }) } }) return { reclassList, tagList, groupedTagList, myFavList }; } function sortByCount(list, key) { return list.sort((a, b) => (b.count - a.count) * 2 + (a[key].toUpperCase() > b[key].toUpperCase() ? 1 : -1)); } function getReclassList(myFavList) { let reclassList = {}; myFavList.forEach(fav => { if (fav.reclass in reclassList) { reclassList[fav.reclass].count++; } else { reclassList[fav.reclass] = { reclass: fav.reclass, translate: '', count: 1 }; } }); return sortByCount(Object.values(reclassList), "reclass"); // [{reclass, translate, count}] } function getTagList(myFavList) { let tagList = {}; myFavList.forEach(fav => { fav.tags.forEach(tag => { if (tag in tagList) { tagList[tag].count++; } else { tagList[tag] = { tag: tag, translate: '', count: 1 }; } }); }); return sortByCount(Object.values(tagList), "tag");// [{tag, translate, count}] } function getGroupedTagList(myFavList) { let groupedTagList = {}; myFavList.forEach(fav => { fav.tags.forEach(fullTag => { let namespace = fullTag.split(":")[0]; let tag = fullTag.split(":")[1]; if (!(namespace in groupedTagList)) { groupedTagList[namespace] = {}; } if (fullTag in groupedTagList[namespace]) { groupedTagList[namespace][fullTag].count++; } else { groupedTagList[namespace][fullTag] = { tag: tag, translate: '', count: 1 }; } }); }); for (let namespace in groupedTagList) { groupedTagList[namespace] = { namespace, translate: '', tags: sortByCount(Object.values(groupedTagList[namespace]), "tag") }; } return Object.values(groupedTagList); // [{namespace, translate, tags:[{tag, translate, count}]}] } function showProgress(message) { let progressDiv = document.querySelector('#progressDiv'); if (!progressDiv) { progressDiv = document.createElement('div'); progressDiv.id = 'progressDiv'; progressDiv.style.position = 'fixed'; progressDiv.style.top = '10px'; progressDiv.style.left = '50%'; progressDiv.style.transform = 'translateX(-50%)'; progressDiv.style.backgroundColor = 'rgba(0,0,0,0.8)'; progressDiv.style.color = 'white'; progressDiv.style.padding = '10px'; progressDiv.style.borderRadius = '5px'; progressDiv.style.zIndex = '10001'; // 调大 z-index document.body.appendChild(progressDiv); } progressDiv.innerText = message; } function hideProgress() { const progressDiv = document.querySelector('#progressDiv'); if (progressDiv) { progressDiv.remove(); } } function showResults(result) { let resultDiv = document.querySelector('#resultDiv'); if (resultDiv) { resultDiv.remove(); } resultDiv = document.createElement('div'); resultDiv.id = 'resultDiv'; resultDiv.translate = false; // https://github.com/EhTagTranslation/EhSyringe/issues/1290 resultDiv.style.position = 'fixed'; resultDiv.style.top = '5%'; resultDiv.style.left = '5%'; resultDiv.style.width = '90%'; resultDiv.style.height = '90%'; resultDiv.style.backgroundColor = 'white'; resultDiv.style.border = '1px solid black'; resultDiv.style.overflow = 'auto'; resultDiv.style.zIndex = '10000'; document.body.appendChild(resultDiv); const closeButton = document.createElement('button'); closeButton.innerText = '关闭'; closeButton.style.position = 'absolute'; closeButton.style.top = '10px'; closeButton.style.right = '10px'; closeButton.style.backgroundColor = 'red'; closeButton.style.color = 'white'; closeButton.style.border = 'none'; closeButton.style.borderRadius = '5px'; closeButton.style.padding = '5px 10px'; closeButton.style.cursor = 'pointer'; closeButton.onclick = () => resultDiv.remove(); resultDiv.appendChild(closeButton); const buttonContainer = document.createElement('div'); buttonContainer.style.display = 'flex'; buttonContainer.style.justifyContent = 'space-between'; buttonContainer.style.marginTop = '20px'; buttonContainer.style.marginBottom = '10px'; const saveRawBtnContainer = document.createElement('div'); saveRawBtnContainer.style.flex = '1'; saveRawBtnContainer.style.textAlign = 'center'; const saveRawBtn = document.createElement('button'); saveRawBtn.id = 'saveBtnJSON'; saveRawBtn.innerText = '保存JSON结果'; saveRawBtn.style.backgroundColor = '#4CAF50'; saveRawBtn.style.color = 'white'; saveRawBtn.style.border = 'none'; saveRawBtn.style.borderRadius = '5px'; saveRawBtn.style.padding = '10px 20px'; saveRawBtn.style.cursor = 'pointer'; saveRawBtnContainer.appendChild(saveRawBtn); const saveTranslatedBtnContainer = document.createElement('div'); saveTranslatedBtnContainer.style.flex = '1'; saveTranslatedBtnContainer.style.textAlign = 'center'; const saveTranslatedBtn = document.createElement('button'); saveTranslatedBtn.id = 'saveBtnHTML'; saveTranslatedBtn.innerText = '保存HTML结果'; saveTranslatedBtn.style.backgroundColor = '#008CBA'; saveTranslatedBtn.style.color = 'white'; saveTranslatedBtn.style.border = 'none'; saveTranslatedBtn.style.borderRadius = '5px'; saveTranslatedBtn.style.padding = '10px 20px'; saveTranslatedBtn.style.cursor = 'pointer'; saveTranslatedBtnContainer.appendChild(saveTranslatedBtn); buttonContainer.appendChild(saveRawBtnContainer); buttonContainer.appendChild(saveTranslatedBtnContainer); resultDiv.appendChild(buttonContainer); const createTable = (data, headers, total = false) => { const table = document.createElement('table'); table.style.border = '1px solid'; table.style.width = '100%'; table.style.borderCollapse = 'collapse'; table.style.textAlign = 'center'; const headerRow = document.createElement('tr'); headerRow.style.height = '30px'; headerRow.style.border = '1px solid'; headers.forEach(header => { const th = document.createElement('th'); th.style.border = '1px solid'; th.innerText = header; headerRow.appendChild(th); }); table.appendChild(headerRow); data.forEach((row, index) => { const tr = document.createElement('tr'); tr.style.height = '30px'; tr.style.border = '1px solid'; tr.title = row.intro || ''; const indexTd = document.createElement('td'); indexTd.innerText = index + 1; tr.appendChild(indexTd); headers.forEach(header => { if (header === 'Index') return; // Skip index column const td = document.createElement('td'); td.style.border = '1px solid'; td.innerText = row[header.toLowerCase()]; tr.appendChild(td); }); table.appendChild(tr); }); if (total) { const totalRow = document.createElement('tr'); totalRow.style.height = '30px'; totalRow.style.border = '1px solid'; const totalTd = document.createElement('td'); totalTd.colSpan = headers.length - 1; totalTd.innerText = 'Total'; totalTd.style.fontWeight = 'bold'; totalTd.style.border = '1px solid'; totalRow.appendChild(totalTd); const countTd = document.createElement('td'); countTd.innerText = data.reduce((sum, row) => sum + row.count, 0); countTd.style.fontWeight = 'bold'; countTd.style.border = '1px solid'; totalRow.appendChild(countTd); table.appendChild(totalRow); } return table; }; const createGroupedTable = (data, headers) => { const table = document.createElement('table'); table.style.border = '1px solid'; table.style.width = '100%'; table.style.borderCollapse = 'collapse'; table.style.textAlign = 'center'; const headerRow = document.createElement('tr'); headerRow.style.height = '30px'; headerRow.style.border = '1px solid'; headers.forEach(header => { const th = document.createElement('th'); th.style.border = '1px solid'; th.innerText = header; headerRow.appendChild(th); }); table.appendChild(headerRow); data.forEach((group, groupIndex) => { const tags = group.tags; tags.forEach((tag, tagIndex) => { const tr = document.createElement('tr'); tr.style.height = '30px'; tr.style.border = '1px solid'; if (tagIndex === 0) { const namespaceTd = document.createElement('td'); namespaceTd.style.border = '1px solid'; namespaceTd.rowSpan = tags.length; namespaceTd.innerText = group.namespace; tr.appendChild(namespaceTd); const namespaceTranslateTd = document.createElement('td'); namespaceTranslateTd.style.border = '1px solid'; namespaceTranslateTd.rowSpan = tags.length; namespaceTranslateTd.innerText = group.translate; tr.appendChild(namespaceTranslateTd); } const indexTd = document.createElement('td'); indexTd.style.border = '1px solid'; indexTd.innerText = tagIndex + 1; indexTd.title = tag.intro || ''; tr.appendChild(indexTd); const tagTd = document.createElement('td'); tagTd.style.border = '1px solid'; tagTd.innerText = tag.tag; tagTd.title = tag.intro || ''; tr.appendChild(tagTd); const translateTd = document.createElement('td'); translateTd.style.border = '1px solid'; translateTd.innerText = tag.translate; translateTd.title = tag.intro || ''; tr.appendChild(translateTd); const countTd = document.createElement('td'); countTd.style.border = '1px solid'; countTd.innerText = tag.count; countTd.title = tag.intro || ''; tr.appendChild(countTd); table.appendChild(tr); }); }) return table; }; const resultContainer = document.createElement('div'); resultContainer.style.display = 'flex'; resultContainer.style.justifyContent = 'space-around'; const rawResultDiv = document.createElement('div'); rawResultDiv.style.width = '90%'; rawResultDiv.style.border = '1px solid black'; rawResultDiv.style.padding = '10px'; const rawResultTitle = document.createElement('h3'); rawResultTitle.innerText = '统计结果'; rawResultDiv.appendChild(rawResultTitle); const rawReclassTitle = document.createElement('h4'); rawReclassTitle.innerText = 'Reclass List'; rawResultDiv.appendChild(rawReclassTitle); rawResultDiv.appendChild(createTable(result.reclassList, ['Index', 'Reclass', 'Translate', 'Count'], true)); const rawTagTitle = document.createElement('h4'); rawTagTitle.innerText = 'Tag List'; rawResultDiv.appendChild(rawTagTitle); rawResultDiv.appendChild(createTable(result.tagList, ['Index', 'Tag', 'Translate', 'Count'])); const rawGroupedTagTitle = document.createElement('h4'); rawGroupedTagTitle.innerText = 'Grouped Tag List'; rawResultDiv.appendChild(rawGroupedTagTitle); rawResultDiv.appendChild(createGroupedTable(result.groupedTagList, ['Namespace', 'Translate', 'Index', 'Tag', 'Translate', 'Count'])); resultContainer.appendChild(rawResultDiv); resultDiv.appendChild(resultContainer); resultDiv.querySelector('#saveBtnJSON').onclick = () => { download('eh_collect.json', JSON.stringify(result, null, 2)); }; resultDiv.querySelector('#saveBtnHTML').onclick = () => { download('eh_collect.html', resultContainer.outerHTML); }; } async function collect(config) { showProgress('正在获取收藏列表...'); const queryUrl = new URL(config.favoritesUrl); const favList = await getFavoritesList(queryUrl); let myFavList = parseFavorites(favList); return myFavList; } function download(filename, data) { const blob = new Blob([data], { type: 'text/plain;charset=utf-8' }); saveAs(blob, filename); } GM_registerMenuCommand("统计收藏", async () => { const collectList = await collect(config); const translate = await getTranslate(config.translationUrl); const result = await translateResult(collectList, translate); hideProgress(); showResults(result); }); })();