Nhentai TSM Tag Catcher

Gets Tags/Details from Nhentai.net Comics for Tachiyomi/Suwayomi/Mihon with improvements!

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         Nhentai TSM Tag Catcher
// @namespace    https://sleazyfork.org/en/users/1261593-john-doe4
// @version      1.3
// @description  Gets Tags/Details from Nhentai.net Comics for Tachiyomi/Suwayomi/Mihon with improvements!
// @author       john doe4
// @match        *://nhentai.net/g/*
// @exclude      *://nhentai.net/g/
// @icon         https://nhentai.net/favicon.ico
// @grant        GM_setClipboard
// @license GPLv3
// ==/UserScript==

(function() {
    'use strict';

    function initPreferences() {
        if (localStorage.getItem('nhTheme') === null) {
            localStorage.setItem('nhTheme', 'light');
        }
        if (localStorage.getItem('nhStatus') === null) {
            localStorage.setItem('nhStatus', '2');
        }
    }
    initPreferences();

        var jsonButton = document.createElement('a');
    jsonButton.id = 'download';
    jsonButton.className = 'btn btn-secondary';
    jsonButton.textContent = 'Catcher';
    jsonButton.style.marginLeft = '2px';
    jsonButton.style.padding = '6px 12px';
    jsonButton.style.lineHeight = '1';
    jsonButton.style.fontSize = '14px';
    jsonButton.style.display = 'inline-flex';
    jsonButton.style.alignItems = 'center';
    jsonButton.style.justifyContent = 'center';
    jsonButton.style.boxSizing = 'border-box';
    jsonButton.addEventListener('click', generateJson);
    document.querySelector('.buttons').appendChild(jsonButton);

    var jsonContainer = document.createElement('div');
    jsonContainer.id = 'tachiyomi-json';
    jsonContainer.style.display = 'none';
    jsonContainer.style.marginTop = '15px';
    jsonContainer.style.width = '100%';
    document.getElementById("info").appendChild(jsonContainer);

    var tachiyomiControls = document.createElement('div');
    tachiyomiControls.innerHTML = `
        <div class="tag-container field-name" style="margin-bottom: 10px;">
            <div class="status-buttons" style="display: flex; gap: 10px; margin-top: 5px;">
                <label class="status-btn" style="flex: 1; position: relative;">
                    <input type="radio" name="status" value="1" ${localStorage.getItem('nhStatus') === '1' ? 'checked' : ''} style="position: absolute; opacity: 0; width: 0; height: 0;">
                    <span class="status-label" style="display: flex; align-items: center; justify-content: center; height: 100%; padding: 6px; text-align: center; border-radius: 4px; font-size: 0.9em; background: ${localStorage.getItem('nhStatus') === '1' ? '#4CAF50' : 'inherit'}; color: ${localStorage.getItem('nhStatus') === '1' ? 'white' : 'inherit'}; cursor: pointer;">Ongoing</span>
                </label>
                <label class="status-btn" style="flex: 1; position: relative;">
                    <input type="radio" name="status" value="2" ${localStorage.getItem('nhStatus') !== '1' ? 'checked' : ''} style="position: absolute; opacity: 0; width: 0; height: 0;">
                    <span class="status-label" style="display: flex; align-items: center; justify-content: center; height: 100%; padding: 6px; text-align: center; border-radius: 4px; font-size: 0.9em; background: ${localStorage.getItem('nhStatus') !== '1' ? '#4CAF50' : 'inherit'}; color: ${localStorage.getItem('nhStatus') !== '1' ? 'white' : 'inherit'}; cursor: pointer;">Completed</span>
                </label>
            </div>
        </div>
        <div class="tag-container field-name" style="margin-bottom: 15px;">
            <textarea type="text" id="description" placeholder="Add description"
                      style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid var(--border-color); margin-top: 5px; background: inherit; font-size: 14px;"></textarea>
        </div>
    `;
    document.getElementById("tags").appendChild(tachiyomiControls);

    function getPreference(key) {
        return localStorage.getItem(`nh${key}`) === 'true';
    }

    function setPreference(key, value) {
        localStorage.setItem(`nh${key}`, value.toString());

        if (key === 'AutoDownload' && value) {
            localStorage.setItem('nhPageLoadDownload', 'false');
            const pageLoadDownload = document.getElementById('page-load-download');
            if (pageLoadDownload) pageLoadDownload.checked = false;
        } else if (key === 'PageLoadDownload' && value) {
            localStorage.setItem('nhAutoDownload', 'false');
            const autoDownload = document.getElementById('auto-download');
            if (autoDownload) autoDownload.checked = false;
        }
    }

    function getTheme() {
        return localStorage.getItem('nhTheme') || 'light';
    }

    function setTheme(theme) {
        localStorage.setItem('nhTheme', theme);
        applyTheme();
        updateThemeLabel();
        updateStatusButtonsStyle();
        updateButtonStyles();
        updateDescriptionBox();
    }

    function updateThemeLabel() {
        const themeLabel = document.getElementById('theme-label');
        if (themeLabel) {
            themeLabel.textContent = getTheme() === 'dark' ? 'Dark' : 'Light';
        }
    }

    function updateStatusButtonsStyle() {
        const theme = getTheme();
        const selectedStatus = localStorage.getItem('nhStatus');
        document.querySelectorAll('.status-label').forEach(label => {
            const radio = label.parentElement.querySelector('input[type="radio"]');
            if (radio.value === selectedStatus) {
                label.style.backgroundColor = '#4CAF50';
                label.style.color = 'white';
            } else {
                label.style.backgroundColor = theme === 'dark' ? '#3d3d3d' : '#f0f0f0';
                label.style.color = theme === 'dark' ? '#ffffff' : '#333333';
            }
        });
    }

    function updateButtonStyles() {
        const theme = getTheme();
        const buttons = ['copy-btn', 'download-btn', 'catch-tags-btn'];
        buttons.forEach(id => {
            const btn = document.getElementById(id);
            if (btn) {
                btn.style.backgroundColor = theme === 'dark' ? '#3d3d3d' : '#f0f0f0';
                btn.style.color = theme === 'dark' ? '#ffffff' : '#333333';
                btn.style.borderColor = theme === 'dark' ? '#444' : '#ddd';
            }
        });
    }

    function updateDescriptionBox() {
        const description = document.getElementById('description');
        if (description) {
            description.style.borderColor = getTheme() === 'dark' ? '#444' : '#ddd';
        }
    }

    function applyTheme() {
        const theme = getTheme();
        const container = document.getElementById('tachiyomi-json');
        if (!container) return;

        const textarea = container.querySelector('textarea');
        if (!textarea) return;

        if (theme === 'dark') {
            container.style.backgroundColor = '#2d2d2d';
            container.style.color = '#ffffff';
            container.style.borderColor = '#444';
            textarea.style.backgroundColor = '#1a1a1a';
            textarea.style.color = '#ffffff';
            textarea.style.borderColor = '#444';
        } else {
            container.style.backgroundColor = '#f5f5f5';
            container.style.color = '#333333';
            container.style.borderColor = '#e0e0e0';
            textarea.style.backgroundColor = '#ffffff';
            textarea.style.color = '#333333';
            textarea.style.borderColor = '#ddd';
        }
        updateStatusButtonsStyle();
        updateButtonStyles();
        updateDescriptionBox();
    }

    function getAuthorFallback() {
        const authorTag = document.querySelectorAll('.tags')[3]?.querySelector('.name');
        if (authorTag) return authorTag.textContent;

        const tagContainers = document.querySelectorAll('.tag-container');
        let groupName = null;

        tagContainers.forEach(container => {
            if (container.textContent.includes('Groups:')) {
                const groupTag = container.querySelector('.tags a');
                if (groupTag) {
                    const groupHref = groupTag.getAttribute('href');
                    groupName = groupHref.split('/group/')[1].replace('/', '');
                }
            }
        });

        if (groupName) return groupName;
        return document.getElementById('gallery_id').textContent.replace('#', '').trim();
    }

    const buttonCooldowns = {};

    function copyToClipboard() {
        const textContent = document.getElementById('json-output')?.value;
        if (textContent) {
            GM_setClipboard(textContent, 'text');

            const btn = document.getElementById('copy-btn');
            if (btn) {
                if (buttonCooldowns['copy']) return;

                const originalText = btn.textContent;
                btn.textContent = 'Copied!';
                buttonCooldowns['copy'] = true;

                setTimeout(() => {
                    btn.textContent = originalText;
                    buttonCooldowns['copy'] = false;
                }, 1500);
            }
        }
    }

    function copyTagsToClipboard() {
        const genreTags = Array.from(document.querySelectorAll('.tags')[2]?.querySelectorAll('.name') || [])
            .map(el => el.textContent)
            .join(', ');

        if (genreTags) {
            GM_setClipboard(genreTags, 'text');

            const btn = document.getElementById('catch-tags-btn');
            if (btn) {
                if (buttonCooldowns['tags']) return;

                const originalText = btn.textContent;
                btn.textContent = 'Tags Copied!';
                buttonCooldowns['tags'] = true;

                setTimeout(() => {
                    btn.textContent = originalText;
                    buttonCooldowns['tags'] = false;
                }, 1500);
            }
        }
    }

    function downloadJson() {
        const textContent = document.getElementById('json-output')?.value;
        if (textContent) {
            const blob = new Blob([textContent], { type: 'application/json' });
            const url = URL.createObjectURL(blob);

            const a = document.createElement('a');
            a.href = url;
            a.download = 'details.json';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        }
    }

    function updateJsonData() {
        const jsonOutput = document.getElementById('json-output');
        if (jsonOutput && jsonOutput.style.display !== 'none') {
            try {
                const genreTags = Array.from(document.querySelectorAll('.tags')[2]?.querySelectorAll('.name') || [])
                    .map(el => el.textContent);

                const jsonData = {
                    title: document.querySelector('.title .pretty')?.textContent || '',
                    author: getAuthorFallback(),
                    artist: getAuthorFallback(),
                    description: document.getElementById('description')?.value || '',
                    genre: genreTags,
                    status: document.querySelector('input[name="status"]:checked')?.value || '2'
                };

                jsonOutput.value = JSON.stringify(jsonData, null, 4);
            } catch (e) {
                console.error('Error updating JSON:', e);
            }
        }
    }

    function generateJson() {
        const jsonDiv = document.getElementById('tachiyomi-json');

        if (jsonDiv.style.display === 'none') {
            jsonDiv.style.display = 'block';
        } else {
            jsonDiv.style.display = 'none';
            return;
        }

        updateJsonData();

        jsonDiv.innerHTML = `
            <div class="tag-container" style="padding: 15px; border-radius: 4px; border: none; width: 100%; box-sizing: border-box;">
                <textarea id="json-output"
                          style="width: 100%; height: 200px; padding: 10px;
                                 border-radius: 4px; margin-bottom: 10px;
                                 font-family: monospace; border: 1px solid var(--border-color);
                                 font-size: 14px; resize: vertical;"></textarea>
                <div style="display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-start; width: 100%;">
                    <div style="display: flex; flex-direction: column; gap: 5px; flex: 1; min-width: 200px;">
                        <div style="display: flex; flex-wrap: wrap; gap: 10px;">
                            <button id="copy-btn" class="btn btn-secondary"
                                    style="padding: 8px 12px; min-width: 120px; font-size: 14px; flex: 1;">
                                Copy to Clipboard
                            </button>
                            <button id="download-btn" class="btn btn-secondary"
                                    style="padding: 8px 12px; min-width: 120px; font-size: 14px; flex: 1;">
                                Download JSON
                            </button>
                            <button id="catch-tags-btn" class="btn btn-secondary"
                                    style="padding: 8px 12px; min-width: 120px; font-size: 14px; flex: 1;">
                                Catch Tags
                            </button>
                        </div>
                        <div style="display: flex; flex-wrap: wrap; gap: 15px; margin-top: 5px;">
                            <label style="display: flex; align-items: center; gap: 5px; color: var(--text-color); font-size: 14px;">
                                <input type="checkbox" id="auto-download"
                                       ${getPreference('AutoDownload') ? 'checked' : ''}
                                       style="margin: 0; accent-color: #4CAF50;">
                                Auto Download
                            </label>
                            <label style="display: flex; align-items: center; gap: 5px; color: var(--text-color); font-size: 14px;">
                                <input type="checkbox" id="page-load-download"
                                       ${getPreference('PageLoadDownload') ? 'checked' : ''}
                                       style="margin: 0; accent-color: #4CAF50;">
                                Download on Load
                            </label>
                        </div>
                    </div>
                    <div style="display: flex; align-items: center; gap: 5px; margin-left: auto; align-self: flex-end;">
                        <span style="color: var(--text-color); font-size: 14px;">Theme:</span>
                        <label class="switch">
                            <input type="checkbox" id="theme-toggle" ${getTheme() === 'dark' ? 'checked' : ''}>
                            <span class="slider round"></span>
                        </label>
                        <span id="theme-label" style="color: var(--text-color); font-size: 14px; min-width: 40px;">${getTheme() === 'dark' ? 'Dark' : 'Light'}</span>
                    </div>
                </div>
            </div>
        `;

        applyTheme();
        updateJsonData();

        document.getElementById('copy-btn')?.addEventListener('click', copyToClipboard);
        document.getElementById('download-btn')?.addEventListener('click', downloadJson);
        document.getElementById('catch-tags-btn')?.addEventListener('click', copyTagsToClipboard);

        document.getElementById('auto-download')?.addEventListener('change', function() {
            setPreference('AutoDownload', this.checked);
        });

        document.getElementById('page-load-download')?.addEventListener('change', function() {
            setPreference('PageLoadDownload', this.checked);
        });

        document.getElementById('theme-toggle')?.addEventListener('change', function() {
            setTheme(this.checked ? 'dark' : 'light');
        });

        document.querySelectorAll('.status-btn input[type="radio"]').forEach(radio => {
            radio.addEventListener('change', function() {
                if (this.checked) {
                    localStorage.setItem('nhStatus', this.value);
                    updateStatusButtonsStyle();
                    updateJsonData();
                }
            });
        });

        document.getElementById('description')?.addEventListener('input', function() {
            updateJsonData();
        });

        if (getPreference('AutoDownload')) {
            setTimeout(downloadJson, 100);
        }
    }

    function initStatusButtons() {
        document.querySelectorAll('.status-btn input[type="radio"]').forEach(radio => {
            radio.addEventListener('change', function() {
                if (this.checked) {
                    localStorage.setItem('nhStatus', this.value);
                    updateStatusButtonsStyle();
                }
            });
        });
        updateStatusButtonsStyle();
    }

    const style = document.createElement('style');
    style.textContent = `
        :root {
            --border-color: #ddd;
            --text-color: #666;
        }
        .dark-theme {
            --border-color: #444;
            --text-color: #aaa;
        }
        .switch {
            position: relative;
            display: inline-block;
            width: 50px;
            height: 24px;
        }
        .switch input {
            opacity: 0;
            width: 0;
            height: 0;
        }
        .slider {
            position: absolute;
            cursor: pointer;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-color: #ccc;
            transition: .4s;
            border-radius: 24px;
        }
        .slider:before {
            position: absolute;
            content: "";
            height: 16px;
            width: 16px;
            left: 4px;
            bottom: 4px;
            background-color: white;
            transition: .4s;
            border-radius: 50%;
        }
        input:checked + .slider {
            background-color: #4CAF50;
        }
        input:checked + .slider:before {
            transform: translateX(26px);
        }
        .status-btn span {
            transition: all 0.3s ease;
        }
        .status-btn:hover span {
            filter: brightness(0.9);
        }
        #tachiyomi-json .tag-container {
            border: none !important;
            background: inherit !important;
        }
        #copy-btn, #download-btn, #catch-tags-btn {
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            transition: all 0.3s ease;
            white-space: nowrap;
        }
        #json-output::-webkit-scrollbar {
            width: 8px;
        }
        #json-output::-webkit-scrollbar-track {
            background: #f1f1f1;
            border-radius: 4px;
        }
        #json-output::-webkit-scrollbar-thumb {
            background: #c1c1c1;
            border-radius: 4px;
        }
        .dark-theme #json-output::-webkit-scrollbar-track {
            background: #3d3d3d;
        }
        .dark-theme #json-output::-webkit-scrollbar-thumb {
            background: #666;
        }
        @media (max-width: 600px) {
            #tachiyomi-json .tag-container {
                padding: 10px !important;
            }
            #json-output {
                height: 150px !important;
                font-size: 13px !important;
            }
            .status-buttons {
                flex-direction: column !important;
            }
            #copy-btn, #download-btn, #catch-tags-btn {
                padding: 8px 10px !important;
                min-width: 100px !important;
                font-size: 13px !important;
            }
            .status-label {
                padding: 8px !important;
            }
        }
    `;
    document.head.appendChild(style);

    function applyThemeClass() {
        if (getTheme() === 'dark') {
            document.body.classList.add('dark-theme');
        } else {
            document.body.classList.remove('dark-theme');
        }
    }
    applyThemeClass();

    setTimeout(initStatusButtons, 100);

    if (getPreference('PageLoadDownload')) {
        window.addEventListener('load', function() {
            setTimeout(function() {
                generateJson();
                if (document.getElementById('tachiyomi-json')?.style.display === 'block') {
                    downloadJson();
                }
            }, 500);
        });
    }
})();