booru keybinds

Keybinds for navigating boorus

Versión del día 18/12/2025. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name            booru keybinds
// @namespace       861ddd094884eac5bea7a3b12e074f34
// @version         3.4.1
// @description     Keybinds for navigating boorus
// @author          Anonymous
// @match           https://rule34.xxx/index.php?page=post&s=list*
// @match           https://rule34.xxx/index.php?page=favorites&s=view&id=*
// @match           https://yande.re/post
// @match           https://yande.re/post?tags=*
// @match           https://yande.re/post?page=*
// @match           https://rule34.us/index.php?r=posts/index*
// @match           https://rule34.us/index.php?r=posts%2Findex*
// @match           https://rule34.us/index.php?r=favorites/view*
// @match           https://rule34.us/index.php?r=favorites%2Fview*
// @exclude         https://rule34.us/index.php?r=posts/view*
// @match           https://gelbooru.com/index.php?page=post&s=list*
// @match           https://gelbooru.com/index.php?page=favorites&s=view&id=*
// @match           https://www.zerochan.net/*
// @exclude         https://www.zerochan.net/pm*
// @exclude         https://www.zerochan.net/moe
// @exclude         https://www.zerochan.net/faq
// @exclude         https://www.zerochan.net/about
// @exclude         https://www.zerochan.net/privacy
// @exclude         https://www.zerochan.net/api
// @exclude         https://www.zerochan.net/upload3
// @exclude         https://www.zerochan.net/forum
// @exclude         https://www.zerochan.net/options*
// @exclude         https://www.zerochan.net/report*
// @match           https://anime-pictures.net/posts?page=*
// @match           https://anime-pictures.net/posts?favorite_by=*
// @match           https://anime-pictures.net/posts?user=*
// @match           https://anime-pictures.net/stars?page=*
// @match           https://konachan.net/post
// @match           https://konachan.net/post?tags=*
// @match           https://konachan.net/post?page=*
// @match           https://kusowanka.com/
// @match           https://kusowanka.com/search/*
// @match           https://kusowanka.com/tag*
// @match           https://kusowanka.com/parod*
// @match           https://kusowanka.com/artist*
// @match           https://kusowanka.com/character*
// @match           https://kusowanka.com/metadata*
// @match           https://tbib.org/index.php?page=post&s=list*
// @match           https://tbib.org/index.php?page=favorites&s=view&id=*
// @match           https://*.booru.org/index.php?page=post&s=list*
// @match           https://*.booru.org/index.php?page=favorites&s=view&id=*
// @match           https://safebooru.org/index.php?page=post&s=list*
// @match           https://safebooru.org/index.php?page=favorites&s=view&id=*
// @match           https://xbooru.com/index.php?page=post&s=list*
// @match           https://xbooru.com/index.php?page=favorites&s=view&id=*
// @match           https://realbooru.com/index.php?page=post&s=list*
// @match           https://realbooru.com/index.php?page=favorites&s=view&id=*
// @match           https://derpibooru.org/*
// @exclude         https://derpibooru.org/filters
// @exclude         https://derpibooru.org/settings*
// @exclude         https://derpibooru.org/*/new
// @exclude         https://derpibooru.org/pages/*
// @exclude         https://derpibooru.org/staff
// @match           https://twibooru.org/*
// @exclude         https://twibooru.org/filters
// @exclude         https://twibooru.org/settings*
// @exclude         https://twibooru.org/*/new
// @exclude         https://twibooru.org/pages/*
// @exclude         https://twibooru.org/staff
// @match           https://manebooru.art/*
// @exclude         https://manebooru.art/filters
// @exclude         https://manebooru.art/settings*
// @exclude         https://manebooru.art/*/new
// @exclude         https://manebooru.art/pages/*
// @exclude         https://manebooru.art/staff
// @match           http://browse.minitokyo.net/gallery*
// @match           http://gallery.minitokyo.net/*
// @match           https://inkbunny.net/submissionsviewall.php*
// @match           https://inkbunny.net/usersviewall.php*
// @match           https://inkbunny.net/gallery/*
// @match           https://hypnohub.net/index.php?page=post&s=list*
// @match           https://hypnohub.net/index.php?page=favorites&s=view&id=*
// @match           https://booru.eu/post/list*
// @match           https://rule34vault.com/*
// @exclude         https://rule34vault.com/trends*
// @exclude         https://rule34vault.com/comments*
// @exclude         https://rule34vault.com/u/*?tab=subs
// @exclude         https://rule34vault.com/u/*?tab=comments
// @exclude         https://rule34vault.com/account*
// @exclude         https://rule34vault.com/post/*
// @exclude         https://rule34vault.com/dmca
// @exclude         https://rule34vault.com/terms
// @exclude         https://rule34vault.com/contact-us
// @exclude         https://rule34vault.com/upgrade-to-premium
// @match           https://rule34.xyz/*
// @exclude         https://rule34.xyz/trends*
// @exclude         https://rule34.xyz/comments*
// @exclude         https://rule34.xyz/u/*?tab=subs
// @exclude         https://rule34.xyz/u/*?tab=comments
// @exclude         https://rule34.xyz/account*
// @exclude         https://rule34.xyz/post/*
// @exclude         https://rule34.xyz/dmca
// @exclude         https://rule34.xyz/terms
// @exclude         https://rule34.xyz/contact-us
// @exclude         https://rule34.xyz/upgrade-to-premium
// @match           https://rule34.world/*
// @exclude         https://rule34.world/trends*
// @exclude         https://rule34.world/comments*
// @exclude         https://rule34.world/u/*?tab=subs
// @exclude         https://rule34.world/u/*?tab=comments
// @exclude         https://rule34.world/account*
// @exclude         https://rule34.world/post/*
// @exclude         https://rule34.world/dmca
// @exclude         https://rule34.world/terms
// @exclude         https://rule34.world/contact-us
// @exclude         https://rule34.world/upgrade-to-premium
// @match           https://furry34.com/*
// @exclude         https://furry34.com/trends*
// @exclude         https://furry34.com/comments*
// @exclude         https://furry34.com/u/*?tab=subs
// @exclude         https://furry34.com/u/*?tab=comments
// @exclude         https://furry34.com/account*
// @exclude         https://furry34.com/post/*
// @exclude         https://furry34.com/dmca
// @exclude         https://furry34.com/terms
// @exclude         https://furry34.com/contact-us
// @exclude         https://furry34.com/upgrade-to-premium
// @grant           none
// @homepageURL     https://greasyfork.org/en/scripts/558617-booru-keybinds
// @supportURL      https://greasyfork.org/en/scripts/558617-booru-keybinds/feedback
// @license         MIT-0
// ==/UserScript==

//
// FEATURES
// - Use A/D or left/right arrow keys to navigate back and forward instead of clicking pagination buttons!
// - Minimally supports browse and search page types.
// - Extended page type support is spotty and may include user uploads/favorites and more.
// - Somewhat intelligent behavior!
//   - Avoids navigation when user has input fields focused.
//   - Avoids inappropriate back navigation on first pages, homepages, etc.
//

//
// CHANGELOG
// v3.4.1       Various bugfixes
//              - Fix rule34vault.com and furry34.com support
//              - Fix rule34.us user favorites page nagivation
//              - Fix navigation for anime-pictures.net
//              - Optimize code with shorthand
// v3.4         Add A/D navigational keybind alternatives for 60% keyboard users
// v3.3         Add support for rule34vault.com, rule34.xyz, rule34.world, furry34.com
// v3.2         Various improvements
//              - Convert remaining URIs matched by regex to globular
//              - Add support for InkBunny user gallery pages
//              - Fix browser-native key events being caught and suppressed (e.g. alt+arrow)
// v3.1         Various improvements
//              - Auto-detect sites running booru-on-rails and derivatives
//              - Move to globular URI matching for more sites
//              - Support more pages across already supported sites
//              - Add in-code URI evaluation for enhanced exclusion logic e.g. zerochan post pages
// v3.0.1       Bugfixes, and improvements to Minitokyo handling
// v3.0         Add support for booru.eu
// v2.6         Add support for minitokyo.net, inkbunny.net, and hypnohub.net
// v2.4         Add support for derpibooru.org, twibooru.org, and manebooru.art
// v2.3         Add support for tbib.org, safebooru.org, xbooru.com, realbooru.com, and booru.org
// v2.2         Add support for zerochan.net, anime-pictures.net, konachan.net, and kusomanka.com
// v2.1         Add support for rule34.us and gelbooru.com
// v2.0         Add support for yande.re
// v1.0         Initial release with support for rule34.xxx
//

//
// TODO
// - Additional site support:
//   - Weasyl.com, FurAffinity.net, and Hentai-Foundry.com
//   - rule34.xxx comment list page
//     https://rule34.xxx/index.php?page=comment&s=user&user=* (pid offset of 15)
// - Add 'last page' detection logic to prevent inappropriate forward navigation.
// - Briefly change the cursor to visually indicate when a requested nav event fails or is rejected.
//

(function() {
    'use strict';

    const DEBUG = false;

    // for domains that use page numbers in an url parameter
    // { domain: [ parameter, first page] }
    const PAGE_DOMAINS = {
        'yande.re': ['page', 1],
        'rule34.us': ['page', 1],
        'zerochan.net': ['p', 1],
        'anime-pictures.net': ['page', 0],
        'konachan.net': ['page', 1],
        'kusowanka.com': ['page', 1],
        'derpibooru.org': ['page', 1],
        'twibooru.org': ['page', 1],
        'manebooru.art': ['page', 1],
        'minitokyo.net': ['page', 1],
        'inkbunny.net': ['page', 1],
        'rule34vault.com': ['page', 1],
        'rule34.xyz': ['page', 1],
        'rule34.world': ['page', 1],
        'furry34.com': ['page', 1],
    };
    // path exceptions to the site minimums above
    const PAGE_PATHS = {
        'rule34.us': [
            { 'r': 'favorites/view' }, 0
        ]
    }
    // for domains that put item count in an url parameter
    // { domain: items per page }
    const PID_DOMAINS = {
        'rule34.xxx': 42,
        'gelbooru.com': 42,
        'tbib.org': 42,
        'booru.org': 20,
        'safebooru.org': 42,
        'xbooru.com': 42,
        'realbooru.com': 42,
        'hypnohub.net': 42
    };
    // booru-on-rails and derivatives (philomena)
    const BOR_SITES = ['derpibooru.org', 'twibooru.org', 'manebooru.art'];
    // rule34.world operations
    const R34WORLD_SITES = ['rule34vault.com', 'rule34.xyz', 'rule34.world'];

    let DEBUG_TEXT = [];
    let PATH = window.location.pathname;
    let PARAMS = new URLSearchParams(window.location.search);
    const FQDN = window.location.hostname;
    let DOMAIN = FQDN;
    const domain_parts = FQDN.split('.');
    if (domain_parts.length >= 3) {
        DOMAIN = (
            domain_parts[domain_parts.length - 2]
            + '.' + domain_parts[domain_parts.length - 1]
        );
    }

    function isPathExcluded() {
        // in place of using regex for a host match exclusion,
        // ^https:\/\/www\.zerochan\.net\/[1-9][0-9]+
        if (DOMAIN === 'zerochan.net'
            && PATH.substring(1)
            && !isNaN(parseInt(PATH.substring(1)))
        ) {
            return true;
        }
        return false;
    }

    function getAlternateMinimumPage() {
        // Check if current domain/path matches PAGE_PATHS config items
        if (DOMAIN in PAGE_PATHS) {
            const pathMatch = PAGE_PATHS[DOMAIN][0];
            const alternateMin = PAGE_PATHS[DOMAIN][1];
            for (const [key, value] of Object.entries(pathMatch)) {
                if (!PARAMS.has(key) || PARAMS.get(key) !== value) {
                    return null;
                }
            }
            return alternateMin;
        }
        return null;
    }

    function detectPaginationType() {
        if (DOMAIN === 'inkbunny.net' && PATH.startsWith('/gallery/')) {
            return 'gallery';
        } else if (DOMAIN in PID_DOMAINS) {
            return 'pid';
        } else if (DOMAIN in PAGE_DOMAINS) {
            return 'page';
        } else if (PATH.startsWith('/post/list')) {
            return 'path';
        }
        return null;
    }

    function getCurrentPage(type) {
        let page;
        if (!type) type = detectPaginationType();
        if (type === 'pid') {
            // assume parameter is named 'pid' and first item is 0
            page = parseInt(
                PARAMS.get(
                    type
                )) || 0;
        } else if (type === 'page') {
            // special case for zerochan frontpage
            if (
                DOMAIN == 'www.zerochan.net'
                && PATH === '/'
                && !PARAMS.has(PAGE_DOMAINS[DOMAIN][0])
            ) {
                page = 0;
            } else {
                // retrieve parameter name from configuration
                // if no such param was passed, return known first page for domain
                const alternateMin = getAlternateMinimumPage();
                const defaultMin = (alternateMin !== null)
                    ? alternateMin : PAGE_DOMAINS[DOMAIN][1];
                page = parseInt(
                    PARAMS.get(PAGE_DOMAINS[DOMAIN][0])
                ) || defaultMin;
            }
        } else if (type === 'path') {
            let path_parts = PATH.split('/');
            if (path_parts.length >= 4) {
                page = parseInt(
                    path_parts[path_parts.length - 1]
                ) || 1;
            } else {
                page = 1;
            }
        } else if (type === 'gallery') {
            // for /gallery/USERNAME/PAGE/HASH, PAGE is at position -2
            let path_parts = PATH.split('/');
            if (path_parts.length >= 4) {
                page = parseInt(
                    path_parts[path_parts.length - 2]
                ) || 1;
            } else {
                page = 1;
            }
        }
        return page;
    }

    function calculatePreviousPage(type, current, pageLength) {
        if (type === 'pid') {
            if (current > 0) {
                return Math.max(0, current - pageLength);
            } else {
                if (DEBUG) console.log(`(current=${current}) <= 0`);
                return false;
            }
        } else if (type === 'page') {
            const alternateMin = getAlternateMinimumPage();
            const minPage = (alternateMin !== null)
                ? alternateMin : PAGE_DOMAINS[DOMAIN][1];
            if (current > minPage) {
                return (current - 1);
            } else {
                if (DEBUG) console.log(`(current=${current}) <= (minPage=${minPage})`);
                return false;
            }
        } else if (['path', 'gallery'].includes(type)) {
            if (current > 1) {
                return (current - 1);
            } else {
                if (DEBUG) console.log(`(current=${current}) <= 1`);
                return false;
            }
        }
    }

    function calculateNextPage(type, current, pageLength) {
        if (type === 'pid') {
            return (current + pageLength);
        } else if (['page', 'path', 'gallery'].includes(type)) {
            return (current + 1);
        }
    }

    function updateQuery(type, dest) {
        if (type === 'pid') {
            // assume parameter is named 'pid'
            PARAMS.set(type, dest);
        } else if (type === 'page') {
            PARAMS.set(PAGE_DOMAINS[DOMAIN][0], dest);
        } else if (type === 'path') {
            let path_parts = PATH.split('/');
            if (path_parts.length >= 4) path_parts.pop();
            path_parts.push(dest.toString());
            PATH = path_parts.join('/');
        } else if (type === 'gallery') {
            // for /gallery/USERNAME/PAGE/HASH, replace PAGE at position -2
            let path_parts = PATH.split('/');
            if (path_parts.length >= 4) {
                path_parts[path_parts.length - 2] = dest.toString();
                PATH = path_parts.join('/');
            }
        }
        return true;
    }

    function reformatUri() {
        // redirect booru-on-rails frontpage to listing page on navigation
        const footer = document.querySelector('#serving_info');
        if (((typeof BOR_SITES === 'object'
                && BOR_SITES.includes(DOMAIN))
            || (footer !== null
                && footer.tagName === 'DIV'
                && (footer.textContent.includes('booru-on-rails')
                    || footer.textContent.includes('philomena'))))
            && PATH === '/'
        ) PATH = (DOMAIN == 'twibooru.org') ? '/posts' : '/images';

        // remove unsupported query string parameters
        // that break navigation when we alter the page param
        if ((typeof R34WORLD_SITES === 'object'
                && R34WORLD_SITES.includes(DOMAIN))
            && PARAMS.has('cursor')
        ) PARAMS.delete('cursor');

        // if there are parameters and we've modified them,
        // reconstruct the search string
        return (PARAMS.size)
            ? `${PATH}?${PARAMS.toString()}`
            : PATH;
    }

    function navigate(direction) {
        PATH = window.location.pathname;
        PARAMS = new URLSearchParams(window.location.search);

        const type = detectPaginationType();
        let current = null;
        current = getCurrentPage(type);

        let pageLength = null;
        if (type === 'pid') pageLength = PID_DOMAINS[DOMAIN];

        let dest = null;
        if (direction === 'previous') {
            dest = calculatePreviousPage(type, current, pageLength);
        } else if (direction === 'next') {
            dest = calculateNextPage(type, current, pageLength);
        }
        // test specifically for false (0 is a valid page number!!)
        if (dest !== 0 && !dest) return false;

        updateQuery(type, dest);

        if (DEBUG) {
            DEBUG_TEXT.push(
                `Keybind caught: direction=${direction}, type=${type}, `
                + `current=${current}, dest=${dest}, pageLength=${pageLength}, `
                + `PARAMS=${PARAMS.toString()}, PATH=${PATH}`
            );
        }

        PATH = reformatUri();
        window.location.href = PATH;
    }

    // function isKeybindExcluded(key) {
    //     if ((['gelbooru.com', 'booru.org'].includes(DOMAIN))
    //         && (key == 'a' || key == 'd')
    //     ) {
    //         return true;
    //     } else {
    //         return false;
    //     }
    // }

    // activation conditions
    if (isPathExcluded()) return false;
    const type = detectPaginationType();
    if (!type) return false;

    document.addEventListener('keydown', function(e) {
        // ignore modifiers
        if (e.ctrlKey || e.altKey || e.shiftKey || e.metaKey) return;

        // ignore keypress when input field is focused
        if (
            e.target.tagName === 'INPUT'
            || e.target.tagName === 'TEXTAREA'
            || e.target.getAttribute('role') == 'search'
        ) return;

        // ignore keys on sites that partially implement our bindings
        // NOTE: native navigation via these keybinds is broken XD
        // if (isKeybindExcluded(e.key.toLowerCase())) return;

        const key = (e.key || '').toLowerCase();
        if (key === 'arrowleft' || key === 'a') {
            e.preventDefault();
            navigate('previous');
        } else if (key === 'arrowright' || key === 'd') {
            e.preventDefault();
            navigate('next');
        }
    });

    console.log(`Keybinds loaded (${type} mode). Use ← → arrow keys to navigate gallery pages.`);
    console.debug(DEBUG_TEXT.join('\n'));
})();