e621 - Skip Blacklisted Posts

Skips blacklisted posts in Next/Prev navigation and returns you to your post after logging in.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         e621 - Skip Blacklisted Posts
// @namespace    http://tampermonkey.net/
// @version      9.1
// @license      MIT
// @description  Skips blacklisted posts in Next/Prev navigation and returns you to your post after logging in.
// @match        https://e621.net/posts*
// @match        https://e621.net/session/new
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    // ─────────────────────────────────────────────────────────────────────────
    // PART 1: Fetch blacklist from user API (cached for the session)
    // ─────────────────────────────────────────────────────────────────────────

    let cachedBlacklist = null;

    function getLoggedInUsername() {
        const userLink = document.querySelector('a[href^="/users/"]');
        if (!userLink) return null;
        const match = userLink.href.match(/\/users\/([^/?]+)/);
        return match ? decodeURIComponent(match[1]) : null;
    }

    async function getBlacklist() {
        if (cachedBlacklist) return cachedBlacklist;
        const username = getLoggedInUsername();
        if (!username) return [];
        try {
            const resp = await fetch(`/users/${encodeURIComponent(username)}.json`);
            if (!resp.ok) return [];
            const data = await resp.json();
            const raw = data.blacklisted_tags || '';
            cachedBlacklist = raw.split('\n').map(t => t.toLowerCase().trim()).filter(Boolean);
            console.log('[e621 Skip] Blacklist loaded:', cachedBlacklist);
            return cachedBlacklist;
        } catch (e) {
            return [];
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // PART 2: Post matching — handles tags, rating: and score: metatags
    // ─────────────────────────────────────────────────────────────────────────

    function getAllTagsFromPost(post) {
        return Object.values(post.tags || {}).flat().map(t => t.toLowerCase());
    }

    function termMatchesPost(term, allTags, post) {
        const negative = term.startsWith('-');
        const t = negative ? term.slice(1) : term;
        let result;

        const ratingMatch = t.match(/^rating:(.+)$/);
        if (ratingMatch) {
            result = (post.rating || '').toLowerCase() === ratingMatch[1][0];
            return negative ? !result : result;
        }

        const scoreMatch = t.match(/^score:([<>]?)(-?\d+)$/);
        if (scoreMatch) {
            const postScore = post.score?.total ?? 0;
            const op = scoreMatch[1];
            const val = parseInt(scoreMatch[2]);
            if (op === '>') result = postScore > val;
            else if (op === '<') result = postScore < val;
            else result = postScore === val;
            return negative ? !result : result;
        }

        result = allTags.includes(t);
        return negative ? !result : result;
    }

    function isPostBlacklisted(post, entries) {
        const allTags = getAllTagsFromPost(post);
        return entries.some(entry =>
            entry.trim().split(/\s+/).every(term => termMatchesPost(term, allTags, post))
        );
    }

    // ─────────────────────────────────────────────────────────────────────────
    // PART 3: Fetch posts from the API
    // ─────────────────────────────────────────────────────────────────────────

    async function fetchPostBatch(tags, fromId, direction) {
        const pageParam = direction === 'next' ? `b${fromId}` : `a${fromId}`;
        const tagsParam = tags ? encodeURIComponent(tags) : '';
        const url = `/posts.json?tags=${tagsParam}&limit=20&page=${pageParam}`;
        try {
            const resp = await fetch(url);
            if (!resp.ok) return [];
            return (await resp.json()).posts || [];
        } catch (e) {
            return [];
        }
    }

    async function findNextNonBlacklisted(currentId, searchTags, direction, entries) {
        let fromId = currentId;
        for (let attempt = 0; attempt < 5; attempt++) {
            let posts = await fetchPostBatch(searchTags, fromId, direction);
            if (!posts.length) return null;
            if (direction === 'prev') posts = posts.reverse();
            for (const post of posts) {
                const bl = isPostBlacklisted(post, entries);
                console.log('[e621 Skip] Checking post', post.id, '— blacklisted:', bl, '| rating:', post.rating);
                if (!bl) return post;
            }
            fromId = posts[posts.length - 1].id;
            await new Promise(r => setTimeout(r, 600));
        }
        return null;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // PART 4: Helpers
    // ─────────────────────────────────────────────────────────────────────────

    function getCurrentPostId() {
        const match = window.location.pathname.match(/\/posts\/(\d+)/);
        return match ? parseInt(match[1]) : null;
    }

    function getSearchTags() {
        const params = new URLSearchParams(window.location.search);
        return params.get('q') || params.get('tags') || '';
    }

    function getNavLinks() {
        return {
            nextLink: document.querySelector('a.nav-link.next'),
            prevLink: document.querySelector('a.nav-link.prev')
        };
    }

    // ─────────────────────────────────────────────────────────────────────────
    // PART 5: Apply and protect correct nav link hrefs
    // ─────────────────────────────────────────────────────────────────────────

    let isRunning = false;
    let lastProcessedId = null;
    let currentCorrectHrefs = { next: null, prev: null };
    let hrefObserver = null;

    function stopObserver() {
        if (hrefObserver) { hrefObserver.disconnect(); hrefObserver = null; }
    }

    function startObserver() {
        stopObserver();
        hrefObserver = new MutationObserver(() => {
            const { nextLink, prevLink } = getNavLinks();
            if (currentCorrectHrefs.next && nextLink && nextLink.href !== currentCorrectHrefs.next) {
                nextLink.href = currentCorrectHrefs.next;
            }
            if (currentCorrectHrefs.prev && prevLink && prevLink.href !== currentCorrectHrefs.prev) {
                prevLink.href = currentCorrectHrefs.prev;
            }
        });
        if (document.body) {
            hrefObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['href'] });
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // PART 6: Main run — update nav links for the current post
    // ─────────────────────────────────────────────────────────────────────────

    async function run(postId) {
        if (isRunning) return;
        if (postId === lastProcessedId) return;

        // Don't interfere with pool navigation
        if (new URLSearchParams(window.location.search).get('pool_id')) {
            console.log('[e621 Skip] Pool detected — skipping.');
            return;
        }

        isRunning = true;
        lastProcessedId = postId;
        currentCorrectHrefs = { next: null, prev: null };
        stopObserver();

        console.log('[e621 Skip] Running on post ID:', postId);

        const entries = await getBlacklist();
        if (entries.length === 0) { isRunning = false; return; }

        const searchTags = getSearchTags();
        const { nextLink, prevLink } = getNavLinks();
        console.log('[e621 Skip] Nav links — next:', nextLink?.href, '| prev:', prevLink?.href);

        startObserver();

        const tasks = [];

        if (nextLink) {
            tasks.push(
                findNextNonBlacklisted(postId, searchTags, 'next', entries).then(post => {
                    if (post) {
                        const tagsPart = searchTags ? `?q=${encodeURIComponent(searchTags)}` : '';
                        currentCorrectHrefs.next = `https://e621.net/posts/${post.id}${tagsPart}`;
                        console.log('[e621 Skip] Next → post', post.id);
                        nextLink.href = currentCorrectHrefs.next;
                    }
                })
            );
        }

        if (prevLink) {
            tasks.push(
                findNextNonBlacklisted(postId, searchTags, 'prev', entries).then(post => {
                    if (post) {
                        const tagsPart = searchTags ? `?q=${encodeURIComponent(searchTags)}` : '';
                        currentCorrectHrefs.prev = `https://e621.net/posts/${post.id}${tagsPart}`;
                        console.log('[e621 Skip] Prev → post', post.id);
                        prevLink.href = currentCorrectHrefs.prev;
                    }
                })
            );
        }

        await Promise.all(tasks);
        console.log('[e621 Skip] Done for post', postId);
        isRunning = false;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // PART 7: Detect single-page navigation via URL polling
    // ─────────────────────────────────────────────────────────────────────────

    let lastUrl = window.location.href;

    function waitForNavLinksToUpdate(expectedId) {
        return new Promise((resolve) => {
            let attempts = 0;
            const interval = setInterval(() => {
                const { nextLink } = getNavLinks();
                if (nextLink && nextLink.href.includes(`/posts/${expectedId}/show_seq`)) {
                    clearInterval(interval);
                    resolve();
                } else if (attempts++ > 30) {
                    clearInterval(interval);
                    resolve();
                }
            }, 100);
        });
    }

    setInterval(async () => {
        const currentUrl = window.location.href;
        if (currentUrl === lastUrl) return;
        lastUrl = currentUrl;
        const postId = getCurrentPostId();
        if (!postId) return;
        console.log('[e621 Skip] URL changed — new post ID:', postId);
        await waitForNavLinksToUpdate(postId);
        run(postId);
    }, 200);

    // ─────────────────────────────────────────────────────────────────────────
    // PART 8: Login redirect
    //
    // Save the current post URL to localStorage on every post page visit.
    // On the login page, confirm it's saved.
    // After login, when landing on the browse page, redirect back there.
    // ─────────────────────────────────────────────────────────────────────────

    const RETURN_KEY = 'e621_skip_return_url';

    // Save current URL on every post page — simple and reliable
    if (window.location.pathname.startsWith('/posts/')) {
        localStorage.setItem(RETURN_KEY, window.location.href);
        console.log('[e621 Skip] Saved return URL:', window.location.href);
    }

    // On login page: set a flag so we know a login just happened
    if (window.location.pathname === '/session/new') {
        localStorage.setItem(RETURN_KEY + '_login', '1');
        console.log('[e621 Skip] Login page — saved URL:', localStorage.getItem(RETURN_KEY));
    }

    // After login, on the browse page: only redirect if the login flag is present
    if (window.location.pathname.startsWith('/posts') && !window.location.pathname.startsWith('/posts/')) {
        const returnUrl = localStorage.getItem(RETURN_KEY);
        const loginFlag = localStorage.getItem(RETURN_KEY + '_login');
        // Always clear the flag regardless
        localStorage.removeItem(RETURN_KEY + '_login');
        if (returnUrl && loginFlag) {
            localStorage.removeItem(RETURN_KEY);
            console.log('[e621 Skip] Redirecting back to:', returnUrl);
            window.location.replace(returnUrl);
        }
    }

    // Initial run on page load
    const initialId = getCurrentPostId();
    if (initialId) run(initialId);

})();