e站收藏统计

获取e站所有收藏,以及对所有标签进行排序以找到你最爱的标签,可按namespace分组,支持翻译

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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);
    });
})();