Sleazy Fork is available in English.

e621 - Skip Blacklisted Posts

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

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 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         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);

})();