Sleazy Fork is available in English.
Skips blacklisted posts in Next/Prev navigation and returns you to your post after logging in.
// ==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);
})();