n-artists

Userscript to favourite nhentai artists

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==UserScript==
// @name         n-artists
// @namespace    URL
// @version      0.2.4
// @description  Userscript to favourite nhentai artists
// @icon         https://nhentai.net/favicon.png
// @author       Sisyphus
// @license      MIT
// @homepage     https://github.com/SisypheOvO
// @match        https://nhentai.net/*
// @run-at       document-end
// @grant        none
// ==/UserScript==


(function () {
    'use strict';

    const FAVORITES_KEY = "favoriteArtists";
    const THUMBS_KEY = "artistsThumbnail";
    function replacer(_key, value) {
        if (value instanceof Map) {
            return { dataType: "Map", value: Array.from(value.entries()) };
        }
        return value;
    }
    function reviver(_key, value) {
        if (typeof value === "object" && value !== null && value.dataType === "Map") {
            return new Map(value.value);
        }
        return value;
    }
    function getFavorites() {
        try {
            return JSON.parse(localStorage.getItem(FAVORITES_KEY) || "[]");
        }
        catch (e) {
            console.error("getFavorites parse error", e);
            return [];
        }
    }
    function saveFavorites(list) {
        localStorage.setItem(FAVORITES_KEY, JSON.stringify(list));
    }
    function getThumbnails() {
        try {
            const raw = localStorage.getItem(THUMBS_KEY);
            if (!raw)
                return new Map();
            return JSON.parse(raw, reviver);
        }
        catch (e) {
            console.error("getThumbnails parse error", e);
            return new Map();
        }
    }
    function saveThumbnails(map) {
        localStorage.setItem(THUMBS_KEY, JSON.stringify(map, replacer));
    }

    const MAX_CONCURRENCY = 3;
    let activeRequests = 0;
    const requestQueue = [];
    function acquireSlot() {
        return new Promise((resolve) => {
            if (activeRequests < MAX_CONCURRENCY) {
                activeRequests++;
                resolve();
            }
            else {
                requestQueue.push(() => {
                    activeRequests++;
                    resolve();
                });
            }
        });
    }
    function releaseSlot() {
        activeRequests--;
        const next = requestQueue.shift();
        if (next)
            next();
    }
    async function fetchWithTimeout(input, init, timeout = 8000) {
        const controller = new AbortController();
        const id = setTimeout(() => controller.abort(), timeout);
        try {
            const resp = await fetch(input, { ...(init || {}), signal: controller.signal });
            return resp;
        }
        finally {
            clearTimeout(id);
        }
    }
    async function fetchArtistThumbnail(artistName) {
        await acquireSlot();
        try {
            const slug = artistName.replace(/\s/g, "-");
            const resp = await fetchWithTimeout(`https://nhentai.net/artist/${slug}/popular`, { credentials: "include" }, 8000);
            if (!resp.ok)
                return null;
            const text = await resp.text();
            const rgx = /https:\/\/t[0-9]{1}\.nhentai\.net\/galleries\/[0-9]{1,8}\/thumb\.jpg/;
            const m = text.match(rgx);
            return m ? m[0] : null;
        }
        catch (e) {
            if (e?.name === "AbortError") {
                console.warn("fetchArtistThumbnail aborted (timeout) for", artistName);
            }
            else {
                console.error("fetchArtistThumbnail error", e);
            }
            return null;
        }
        finally {
            releaseSlot();
        }
    }

    async function toggleFavorite(artistName) {
        try {
            const favs = getFavorites();
            const thumbs = getThumbnails();
            const idx = favs.indexOf(artistName);
            let added = false;
            if (idx === -1) {
                favs.push(artistName);
                added = true;
                // async fetch thumbnail and persist
                fetchArtistThumbnail(artistName)
                    .then((url) => {
                    if (url) {
                        thumbs.set(artistName, url);
                        saveThumbnails(thumbs);
                    }
                })
                    .catch((e) => console.warn("thumbnail fetch failed", e));
            }
            else {
                favs.splice(idx, 1);
                thumbs.delete(artistName);
                saveThumbnails(thumbs);
            }
            saveFavorites(favs);
            return added;
        }
        catch (e) {
            console.error("toggleFavorite error", e);
            return false;
        }
    }
    function updateButtonState(added, btn, artistName) {
        btn.classList.toggle("is-favorite", added);
        const icon = btn.querySelector("i");
        if (!icon)
            return;
        if (added) {
            icon.classList.remove("far");
            icon.classList.add("fa");
            icon.style.color = "var(--accent)";
        }
        else {
            icon.classList.remove("fa");
            icon.classList.add("far");
            icon.style.removeProperty("color");
        }
    }

    function createBtnArtistPage(className, title) {
        const wrapper = document.createElement("span");
        wrapper.style = "padding: 0.13em 0.26em; margin-right: 0.13em; display: inline-flex; align-items: center; justify-content: center; background-color: var(--border); border-radius: .3em; vertical-align: middle;";
        const el = document.createElement("i");
        el.className = className;
        el.style.cursor = "pointer";
        el.style.lineHeight = "inherit";
        el.style.margin = "0";
        wrapper.appendChild(el);
        return wrapper;
    }
    function injectFavBtns2ArtistPage() {
        const header = document.querySelector("h1");
        if (!header)
            return;
        if (header.querySelector(".favoriteArtistButton"))
            return;
        const match = window.location.pathname.match(/\/artist\/([^/]+)\//);
        const artistName = match ? decodeURIComponent(match[1].replace(/-/g, " ")) : null;
        if (!artistName)
            return;
        const btn = createBtnArtistPage("far fa-heart favoriteArtistButton");
        btn.dataset.artist = artistName;
        const added = getFavorites().includes(artistName);
        updateButtonState(added, btn);
        btn.addEventListener("click", async (event) => {
            event.preventDefault();
            event.stopPropagation();
            const added = await toggleFavorite(artistName);
            updateButtonState(added, btn);
        });
        header.insertAdjacentElement("afterbegin", btn);
    }
    function initArtistPage() {
        try {
            injectFavBtns2ArtistPage();
            if (window.__nArtistsArtistObserver)
                return;
            const observer = new MutationObserver(() => {
                injectFavBtns2ArtistPage();
            });
            observer.observe(document.body, {
                childList: true,
                subtree: true,
            });
            window.__nArtistsArtistObserver = observer;
        }
        catch (e) {
            console.error("initArtistPage error", e);
        }
    }

    function createBtnWorkPage(className, title) {
        const wrapper = document.createElement("span");
        wrapper.style = "padding: 0.13em 0.26em; display: inline-flex; align-items: center; justify-content: center; background-color: var(--border); border-top-left-radius: .3em; border-bottom-left-radius: .3em;";
        const el = document.createElement("i");
        el.className = className;
        el.style.cursor = "pointer";
        wrapper.appendChild(el);
        return wrapper;
    }
    function injectFavBtns2WorkPage() {
        const tagContainers = Array.from(document.querySelectorAll("div.tag-container.field-name:not(.hidden)"));
        const artistContainer = tagContainers.find((el) => el.textContent && el.textContent.includes("Artists:"));
        if (!artistContainer)
            return;
        const artistTagChips = Array.from(artistContainer.querySelectorAll("a.tagchip[href^='/artist/']"));
        for (const artistTagChip of artistTagChips) {
            if (artistTagChip.querySelector(".favoriteArtistButton"))
                continue;
            const href = artistTagChip.getAttribute("href");
            const match = href?.match(/\/artist\/([^/?#]+)/);
            const artistName = match ? decodeURIComponent(match[1].replace(/-/g, " ")) : null;
            if (!artistName)
                continue;
            const btn = createBtnWorkPage("far fa-heart favoriteArtistButton");
            btn.dataset.artist = artistName;
            const added = getFavorites().includes(artistName);
            updateButtonState(added, btn);
            btn.addEventListener("click", async (event) => {
                event.preventDefault();
                event.stopPropagation();
                const added = await toggleFavorite(artistName);
                updateButtonState(added, btn);
            });
            artistTagChip.insertAdjacentElement("afterbegin", btn);
        }
    }
    /**
     * init artwork page by adding favorite buttons to artist tags
     */
    function initWorkPage() {
        try {
            injectFavBtns2WorkPage();
            if (window.__nArtistsWorkObserver)
                return;
            const observer = new MutationObserver(() => {
                injectFavBtns2WorkPage();
            });
            observer.observe(document.body, {
                childList: true,
                subtree: true,
            });
            window.__nArtistsWorkObserver = observer;
        }
        catch (e) {
            console.error("initWorkPage error", e);
        }
    }

    let showingFavoriteArtists = false;
    let lastFavoritesRouteKey = null;
    let importInputEl = null;
    function getFavoritesWorkspace() {
        return document.getElementById("favcontainer");
    }
    function updateDisplayButtonLabel(button) {
        button.textContent = showingFavoriteArtists ? "Show Favorite Doujins" : "Show Favorite Artists";
        button.setAttribute("aria-pressed", showingFavoriteArtists ? "true" : "false");
    }
    function renderFavorites(container) {
        const favs = getFavorites();
        const thumbs = getThumbnails();
        container.innerHTML = "";
        for (const artist of favs) {
            const thumb = thumbs.get(artist) || "";
            const root = document.createElement("div");
            root.className = "gallery-favorite";
            root.setAttribute("artist-name", artist);
            const gallery = document.createElement("div");
            gallery.className = "gallery";
            gallery.style = "margin-bottom: 4px;";
            const link = document.createElement("a");
            link.className = "cover";
            link.href = `/artist/${artist.replace(/\s/g, "-")}/`;
            const img = document.createElement("img");
            img.width = 250;
            img.height = 353;
            if (thumb)
                img.src = thumb;
            img.alt = artist;
            img.style = "position: relative; object-fit: cover; width: 100%; height: 100%;";
            const caption = document.createElement("div");
            caption.className = "caption";
            caption.textContent = artist;
            caption.style = "position: relative;";
            link.appendChild(img);
            link.appendChild(caption);
            link.style = "display: inline-flex; padding: 0; margin: 0; flex-direction: column; align-items: center;";
            gallery.appendChild(link);
            root.appendChild(gallery);
            container.appendChild(root);
        }
    }
    function getFavoriteArtistsTxt() {
        const favorites = getFavorites();
        const thumbs = getThumbnails();
        return favorites
            .map((artist) => {
            const thumbnail = thumbs.get(artist) || "";
            return `${artist}\t${thumbnail}`;
        })
            .join("\n");
    }
    function downloadTxtFile(filename, content) {
        const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
        const url = URL.createObjectURL(blob);
        const link = document.createElement("a");
        link.href = url;
        link.download = filename;
        document.body.appendChild(link);
        link.click();
        link.remove();
        URL.revokeObjectURL(url);
    }
    function formatTimestamp(date) {
        const pad = (value) => String(value).padStart(2, "0");
        return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
    }
    function parseFavoriteArtistsTxt(text) {
        const seen = new Set();
        const artists = [];
        const thumbnails = new Map();
        for (const rawLine of text.split(/\r?\n/)) {
            const line = rawLine.trim();
            if (!line)
                continue;
            const [artistPart, thumbnailPart = ""] = line.split("\t");
            const artist = artistPart.trim();
            if (!artist)
                continue;
            if (seen.has(artist))
                continue;
            seen.add(artist);
            artists.push(artist);
            const thumbnail = thumbnailPart.trim();
            if (thumbnail) {
                thumbnails.set(artist, thumbnail);
            }
        }
        return { artists, thumbnails };
    }
    function getDiffSummary(currentList, importedList) {
        const currentSet = new Set(currentList);
        const importedSet = new Set(importedList);
        const added = importedList.filter((artist) => !currentSet.has(artist));
        const removed = currentList.filter((artist) => !importedSet.has(artist));
        return { added, removed };
    }
    function isLargeImportDiff(currentCount, addedCount, removedCount) {
        const threshold = Math.max(10, Math.ceil(currentCount * 0.2));
        return addedCount >= threshold || removedCount >= threshold;
    }
    function formatArtistSample(list, limit = 5) {
        if (list.length === 0)
            return "(none)";
        return list.slice(0, limit).join(", ") + (list.length > limit ? ` ... (+${list.length - limit})` : "");
    }
    function confirmImportReplacement(currentList, importedList) {
        const { added, removed } = getDiffSummary(currentList, importedList);
        const largeDiff = isLargeImportDiff(currentList.length, added.length, removed.length);
        const warning = largeDiff ? "\n\nWarning: the difference is large. Replacing the current list is irreversible. You may want to back up first." : "";
        const message = ["Review the import file before continuing.", `Current artists: ${currentList.length}`, `Imported artists: ${importedList.length}`, `To be added: ${added.length}`, `To be removed: ${removed.length}`, `Added sample: ${formatArtistSample(added)}`, `Removed sample: ${formatArtistSample(removed)}`, warning, "", "Continue import and overwrite the current list?"].join("\n");
        return window.confirm(message);
    }
    function refreshFavoriteArtistsView() {
        if (!showingFavoriteArtists)
            return;
        const panel = getArtistsPanel();
        if (!panel)
            return;
        renderFavorites(panel);
    }
    async function importFavoriteArtistsFile(file) {
        const text = await file.text();
        const { artists, thumbnails } = parseFavoriteArtistsTxt(text);
        const currentFavorites = getFavorites();
        if (!confirmImportReplacement(currentFavorites, artists)) {
            return;
        }
        const existingThumbnails = getThumbnails();
        const nextThumbnails = new Map(existingThumbnails);
        for (const artist of artists) {
            const parsedThumbnail = thumbnails.get(artist);
            if (parsedThumbnail) {
                nextThumbnails.set(artist, parsedThumbnail);
                continue;
            }
            const existingThumbnail = existingThumbnails.get(artist);
            if (existingThumbnail) {
                nextThumbnails.set(artist, existingThumbnail);
                continue;
            }
            try {
                const fetchedThumbnail = await fetchArtistThumbnail(artist);
                if (fetchedThumbnail) {
                    nextThumbnails.set(artist, fetchedThumbnail);
                }
            }
            catch (e) {
                console.warn("fetch thumbnail during import failed", artist, e);
            }
        }
        saveFavorites(artists);
        saveThumbnails(nextThumbnails);
        refreshFavoriteArtistsView();
    }
    function getArtistsPanel() {
        return document.getElementById("favorite-artists-panel");
    }
    function setArtworkPaginationVisible(visible) {
        const isMobile = window.matchMedia("(max-width: 599px)").matches;
        const activeSelector = isMobile ? "mobile-pagination" : "desktop-pagination";
        console.log("Setting artwork pagination visible?", visible, "activeSelector", activeSelector);
        for (const pagination of document.querySelectorAll(".pagination")) {
            const paginationElement = pagination;
            console.log("Pagination element", paginationElement, "activeSelector", activeSelector);
            const isActivePagination = paginationElement.classList.contains(activeSelector);
            console.log("Is active pagination?", isActivePagination);
            paginationElement.style.display = visible && isActivePagination ? "block" : "none";
            console.log("Set pagination display to", paginationElement.style.display);
        }
    }
    function showFavoriteArtists(button) {
        const workspace = getFavoritesWorkspace();
        if (!workspace)
            return;
        let panel = getArtistsPanel();
        if (!panel) {
            panel = document.createElement("div");
            panel.id = "favorite-artists-panel";
            panel.style = "margin-top: 0.75em;";
            workspace.insertAdjacentElement("beforebegin", panel);
        }
        workspace.style.display = "none";
        setArtworkPaginationVisible(false);
        panel.style.display = "block";
        renderFavorites(panel);
        showingFavoriteArtists = true;
        if (button)
            updateDisplayButtonLabel(button);
    }
    function showFavoriteArtworks(button) {
        const workspace = getFavoritesWorkspace();
        if (!workspace)
            return;
        const panel = getArtistsPanel();
        if (panel)
            panel.remove();
        workspace.style.display = "block";
        setArtworkPaginationVisible(true);
        showingFavoriteArtists = false;
        if (button)
            updateDisplayButtonLabel(button);
    }
    function syncFavoritesRouteState() {
        const currentRouteKey = `${window.location.pathname}${window.location.search}`;
        if (lastFavoritesRouteKey !== currentRouteKey) {
            lastFavoritesRouteKey = currentRouteKey;
            showingFavoriteArtists = false;
            const panel = getArtistsPanel();
            if (panel)
                panel.remove();
            const workspace = getFavoritesWorkspace();
            if (workspace)
                workspace.style.display = "block";
            setArtworkPaginationVisible(true);
        }
    }
    function injectDisplayButtonStyle() {
        if (document.getElementById("favorites-display-button-style"))
            return;
        const style = document.createElement("style");
        style.id = "favorites-display-button-style";
        style.textContent = `
    #displayFavoriteArtists, #exportFavoriteArtists, #importFavoriteArtists {
        margin-left: 0.5em;
        background-color: var(--border);
        transition: background-color 0.2s ease;
    }

    #displayFavoriteArtists:hover, #exportFavoriteArtists:hover, #importFavoriteArtists:hover {
        background-color: var(--accent-hover);
    }
`;
        document.head.appendChild(style);
    }
    function injectDisplayButton() {
        const element = document.getElementById("favorites-random-button");
        if (!element)
            return;
        if (document.getElementById("displayFavoriteArtists"))
            return;
        const displayButton = document.createElement("button");
        displayButton.id = "displayFavoriteArtists";
        displayButton.className = "btn";
        displayButton.type = "button";
        updateDisplayButtonLabel(displayButton);
        element.parentNode?.insertBefore(displayButton, element.nextSibling || null);
        displayButton.addEventListener("click", () => {
            if (showingFavoriteArtists) {
                showFavoriteArtworks(displayButton);
            }
            else {
                showFavoriteArtists(displayButton);
            }
        });
        const exportButton = document.createElement("button");
        exportButton.id = "exportFavoriteArtists";
        exportButton.className = "btn";
        exportButton.type = "button";
        exportButton.textContent = "Export Artists TXT";
        exportButton.style.marginLeft = "0.5em";
        exportButton.addEventListener("click", () => {
            downloadTxtFile(`favorite-artists-${formatTimestamp(new Date())}.txt`, `${getFavoriteArtistsTxt()}\n`);
        });
        const importButton = document.createElement("button");
        importButton.id = "importFavoriteArtists";
        importButton.className = "btn";
        importButton.type = "button";
        importButton.textContent = "Import Artists TXT";
        importButton.style.marginLeft = "0.5em";
        importButton.addEventListener("click", () => {
            importInputEl?.click();
        });
        if (!importInputEl) {
            importInputEl = document.createElement("input");
            importInputEl.type = "file";
            importInputEl.accept = ".txt,text/plain";
            importInputEl.hidden = true;
            importInputEl.addEventListener("change", async () => {
                const file = importInputEl?.files?.[0];
                if (!file)
                    return;
                try {
                    await importFavoriteArtistsFile(file);
                }
                catch (e) {
                    console.error("importFavoriteArtistsFile error", e);
                }
                finally {
                    if (importInputEl)
                        importInputEl.value = "";
                }
            });
            document.body.appendChild(importInputEl);
        }
        element.insertAdjacentElement("afterend", importButton);
        element.insertAdjacentElement("afterend", exportButton);
    }
    function initFavoritesPage() {
        try {
            injectDisplayButtonStyle();
            syncFavoritesRouteState();
            injectDisplayButton();
            if (window.__nArtistsFavoritesObserver)
                return;
            const observer = new MutationObserver(() => {
                injectDisplayButton();
            });
            observer.observe(document.body, {
                childList: true,
                subtree: true,
            });
            window.__nArtistsFavoritesObserver = observer;
        }
        catch (e) {
            console.error("initFavoritesPage error", e);
        }
    }

    function smallCSSInject() {
        if (document.getElementById("favorites-search-style"))
            return;
        const style = document.createElement("style");
        style.id = "favorites-search-style";
        style.textContent = `
    #favorites-search {
    display: inline-flex;
}`;
        document.head.appendChild(style);
    }
    function runRoute() {
        const url = window.location.href;
        const reg_url_1 = /https:\/\/nhentai\.net\/g\/[0-9]*\/$/;
        const reg_url_2 = /^https:\/\/nhentai\.net\/user\/favorites(?!\/)(?:\?.*)?$/;
        const reg_url_3 = /https:\/\/nhentai\.net\/artist\/[^/]+\/$/;
        if (reg_url_1.test(url)) {
            initWorkPage();
        }
        else if (reg_url_2.test(url)) {
            smallCSSInject();
            initFavoritesPage();
        }
        else if (reg_url_3.test(url)) {
            initArtistPage();
        }
    }
    function installRouteWatcher() {
        if (window.__nArtistsRouteWatcherInstalled)
            return;
        const runAndRemember = () => {
            runRoute();
        };
        const wrapHistoryMethod = (methodName) => {
            const original = history[methodName];
            history[methodName] = function (...args) {
                const result = original.apply(history, args);
                window.dispatchEvent(new Event("n-artists:locationchange"));
                return result;
            };
        };
        wrapHistoryMethod("pushState");
        wrapHistoryMethod("replaceState");
        window.addEventListener("popstate", runAndRemember);
        window.addEventListener("hashchange", runAndRemember);
        window.addEventListener("n-artists:locationchange", runAndRemember);
        window.__nArtistsRouteWatcherInstalled = true;
    }
    installRouteWatcher();
    runRoute();

})();