e621 - Skip Blacklisted Posts

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

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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

})();