Kemono Fixer

Allow you to blacklist creators on Kemono, and more.

Versión del día 05/10/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.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este 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        Kemono Fixer
// @namespace   DKKKNND
// @license     WTFPL
// @match       https://kemono.cr/*
// @match       https://coomer.st/*
// @grant       GM_registerMenuCommand
// @grant       GM_setValue
// @grant       GM_getValue
// @version     1.0
// @author      Kaban
// @description Allow you to blacklist creators on Kemono, and more.
// ==/UserScript==
(function() {
"use strict";
// ==<Options>==
const OPTIONS = JSON.parse(GM_getValue("OPTIONS", `{
  "adapt_post_card_height_to_thumbnail": false,
  "sort_posts_by_publish_date": true,
  "hide_posts_with_no_thumbnail": true,
  "hide_posts_from_blacklisted_creators": true,
  "hide_posts_shown_on_previous_pages": true
}`));
let BLACKLIST_MODE = false;
let BLACKLIST_RAW = JSON.parse(GM_getValue("BLACKLIST", `{}`));
let BLACKLIST = Object.keys(BLACKLIST_RAW);
// ==</Options>==

// ==<Menu>==
updateMenu("Adaptive Post Card Height",
  "adapt_post_card_height_to_thumbnail", true);
updateMenu("Sort Posts by Publish Date",
  "sort_posts_by_publish_date", true);
updateMenu("Hide Posts with No Thumbnail",
  "hide_posts_with_no_thumbnail", false);
updateMenu("Hide Posts from Blacklisted Creators",
  "hide_posts_from_blacklisted_creators", false);
updateMenu("Hide Posts shown on Previous Pages",
  "hide_posts_shown_on_previous_pages", false);
GM_registerMenuCommand(
  "---------------------------------------", null, { autoClose: false });
GM_registerMenuCommand(
  "▶ Enter Blacklist Mode", blacklistMode, { id: "blacklistMode" });
GM_registerMenuCommand(
  "⬆ Export Blacklist to Clipboard", exportBlacklist);
GM_registerMenuCommand(
  "⬇ Import Blacklist from Clipboard", importBlacklist);

function updateMenu(menuName, optionName, reloadPage = false) {
  GM_registerMenuCommand(
    `${OPTIONS[optionName] ? "☑" : "☐"} ${menuName}`, () => {
      updateOption(optionName, menuName, reloadPage)
    }, {
      id: optionName,
      title: `${menuName}${reloadPage ? " (Reloads current page)" : ""}`
    }
  );
}

function updateOption(optionName, menuName, reloadPage) {
  OPTIONS[optionName] = !OPTIONS[optionName];
  updateMenu(menuName, optionName, reloadPage);
  GM_setValue("OPTIONS", JSON.stringify(OPTIONS));
  if (reloadPage) {
    window.location.reload();
  } else {
    updateStyles();
  }
}

function updateStyles() {
  if(OPTIONS.hide_posts_with_no_thumbnail) {
    addStyle(CSS.hideNoThumbnail, "hide_no_thumbnail");
  } else {
    removeStyle("hide_no_thumbnail");
  }
  if(OPTIONS.hide_posts_from_blacklisted_creators) {
    addStyle(CSS.hideBlacklisted, "hide_blacklisted");
  } else {
    removeStyle("hide_blacklisted");
  }
  if(OPTIONS.hide_posts_shown_on_previous_pages) {
    addStyle(CSS.hideAlreadyShown, "hide_already_shown");
  } else {
    removeStyle("hide_already_shown");
  }
  if (BLACKLIST_MODE) {
    addStyle(CSS.blacklistMode, "blacklist_mode");
    removeStyle("hide_blacklisted");
  } else {
    removeStyle("blacklist_mode");
    if(OPTIONS.hide_posts_from_blacklisted_creators)
      addStyle(CSS.hideBlacklisted, "hide_blacklisted");
  }
}

function blacklistMode() {
  BLACKLIST_MODE = !BLACKLIST_MODE;
  updateStyles();
  if (BLACKLIST_MODE) {
    GM_registerMenuCommand(
      "◀ Exit Blacklist Mode", blacklistMode, { id: "blacklistMode" });

  } else {
    GM_registerMenuCommand(
      "▶ Enter Blacklist Mode", blacklistMode, { id: "blacklistMode" });

  }
}

function exportBlacklist() {
  let length = BLACKLIST.length;
  if (length === 0) {
    alert("Blacklist is empty, nothing to export.")
  } else {
    let message = `Do you want to export blacklist (${length} ${length > 1 ? "entries" : "entry"}) to clipboard?`;
    if (confirm(message)) {
      setTimeout(exportToClipboard, 500);
    }
  }
}

async function exportToClipboard() {
  try {
    await navigator.clipboard.writeText(JSON.stringify(BLACKLIST_RAW, null, 2));
    alert(`Exported blacklist (${BLACKLIST.length} entries) to clipboard as JSON.`);
  } catch (error) {
    alert(`Export to clipboard failed:\n${error.message}`);
  }
}

function importBlacklist() {
  let message = "Do you want to import blacklist from clipboard?";
  if (confirm(message)) {
    let length = BLACKLIST.length;
    if (length > 0) {
      message = `Current blacklist (${length} ${length > 1 ? "entries" : "entry"}) will be overwritten!`;
      if (!confirm(message)) return;
    }
    setTimeout(importFromClipboard, 500);
  }
}

async function importFromClipboard() {
  try {
    let rawClipboard = await navigator.clipboard.readText();
    if (!rawClipboard) {
      alert("Clipboard read empty.\n(Permission denied?)");
      return;
    }
    let json;
    if (rawClipboard.startsWith('{')) {
      try {
        json = JSON.parse(rawClipboard);
      } catch (error) {
        alert(`Parse JSON failed:\n${error.message}`)
      }
    } else {
      // backward compatibility with data from old version script
      let lines = rawClipboard.split('\n');
      let regex = /"([^"]+)"[,\s]*\/\/\s*(.*?)\s*$/;
      json = {};
      for (let i = 0; i < lines.length; i++) {
        let match = lines[i].match(regex);
        if (match) {
          json[match[1]] = match[2];
        }
      }
    }
    let length = Object.keys(json).length;
    if (length === 0) {
      let message = "Found no valid entry from the clipboard.\nEnter \"clear\" to clear the blacklist.";
      if (prompt(message, "")?.toLowerCase() === "clear") {
        BLACKLIST_RAW = {};
        BLACKLIST = [];
        GM_setValue("BLACKLIST", "{}");
        alert("Blacklist cleared.");
      } else {
        alert("Import aborted.");
      }
    } else {
      BLACKLIST_RAW = json;
      BLACKLIST = Object.keys(BLACKLIST_RAW);
      GM_setValue("BLACKLIST", JSON.stringify(BLACKLIST_RAW));
      alert(`Imported ${length} blacklist ${length > 1 ? "entries" : "entry"} from clipboard.`)
    }
  } catch (error) {
    alert(`Import from clipboard failed:\n${error.message}`);
  }
}
// ==</Menu>==

// ==<Helper Functions>==
function gOne(id) {
  return document.getElementById(id);
}
function qOne(selector) {
  return document.querySelector(selector);
}
Element.prototype.qOne = function(selector) {
  return this.querySelector(selector);
}
function qAll(selector) {
  let results = document.querySelectorAll(selector);
  return results.length === 0 ? null : results;
}
Element.prototype.qAll = function(selector) {
  let results = this.querySelectorAll(selector);
  return results.length === 0 ? null : results;
}
function createKfElement(type, id = null, content = null) {
  let element = document.createElement(type);
  if (id) {
    element.id = `kf-${id}`;
    let oldElement = gOne(element.id);
    if (oldElement) oldElement.remove();
  }
  if (content) element.textContent = content;
  return element;
}
HTMLElement.prototype.addKfClass = function(className) {
  return this.classList.add(`kf-${className}`);
}
HTMLElement.prototype.setKfAttr = function(attributeName, value) {
  return this.setAttribute(`data-kf-${attributeName}`, value);
}
HTMLElement.prototype.getKfAttr = function(attributeName) {
  return this.getAttribute(`data-kf-${attributeName}`);
}
HTMLElement.prototype.getDataAttr = function(attributeName) {
  return this.getAttribute(`data-${attributeName}`);
}
// ==</Helper Functions>==

// ==<Mutation Observer>==
const observer = new MutationObserver(onMutation);
observer.observe(document.body, { childList: true, subtree: true });

function onMutation(mutations, observer) {
  // stop observer before DOM manipulation
  observer.disconnect();
  // pathname-based dispatch
  let path = window.location.pathname;
  if (path === "/posts" || path === "/posts/popular") {
    addStyle(CSS.posts);
    fixPosts();
  } else {
    let segments = path.split('/').filter(segment => segment);
    if (segments[1] === "user") {
      // {service}/user/{user_id}
      if (segments.length === 3) {
        //fixCreatorPosts();
      } else if (segments[3] === "post") {
        if (
          // {service}/user/{user_id}/post/{post_id}
          segments.length === 5 ||
          // {service}/user/{user_id}/post/{post_id}/revision/{revision_id}
          segments.length === 7 && segments[5] === "revision"
        ) {
          //fixCurrentPost();
        }
      // discord/server/{server_id}/{room_id}
      } else if (segments.length === 4 && segments[0] === "discord") {
        //fixDiscord();
      }
    }
  }
  // restart observer
  observer.observe(document.body, { childList: true, subtree: true });
}
// ==</Mutation Observer>==

// ==<Page Fixing Functions>==
function kfExist(id) {
  let element = gOne(`kf-${id}`);
  if (!element) return false;
  if (element.getKfAttr("href") === window.location.href) return true;
  element.remove();
  return false;
}

function addStyle(css, id = "style") {
  if (kfExist(id)) return;
  let style = createKfElement("style", id, css);
  style.setKfAttr("href", window.location.href);
  document.head.append(style);
}

function removeStyle(id = "style") {
  let style = gOne(`kf-${id}`);
  if(style) style.remove();
}

function fixPosts() {
  if (kfExist("postCardContainer")) return;

  let srcPostCardContainer = qOne(".card-list__items");
  if (!srcPostCardContainer) return;
  let srcPostCards = srcPostCardContainer.qAll(".post-card");
  if (!srcPostCards) return;

  let newPostCardContainer = createKfElement("div", "postCardContainer");
  let newPostCards = [];
  for(const card of srcPostCards) {
    let newCard = card.cloneNode(true);
    if (OPTIONS.sort_posts_by_publish_date) {
      let timeStamp = new Date(newCard.qOne("time").dateTime).getTime();
      newCard.setKfAttr("published", timeStamp);
    }
    newPostCards.push(newCard);
  }
  if (OPTIONS.sort_posts_by_publish_date) {
    newPostCards = newPostCards.sort((a, b) =>
      b.getKfAttr("published") - a.getKfAttr("published")
    );
  }

  let hiddenPosts = {
    noThumbnail: 0,
    creatorBlacklisted: 0,
    blacklistedCreators: [],
    alreadyShown: 0
  };
  let shownPosts = [];
  let currentOffset = parseInt(new URLSearchParams(window.location.search).get("o")) || 0;
  if (currentOffset > parseInt(sessionStorage.getItem("lastOffset")))
    shownPosts = JSON.parse(sessionStorage.getItem("shownPosts")) || [];
  for(const card of newPostCards) {
    let thumbnail = card.qOne(".post-card__image");
    if (!thumbnail) {
      hiddenPosts.noThumbnail++;
      card.addKfClass("no_thumbnail");
    } else {
      thumbnail.addEventListener("load", fixThumbnailAspectRatio);
    }

    let blacklistControl = createKfElement("div", "blacklist_control")
    blacklistControl.addEventListener("click", preventDefault);
    let blacklistButton = document.createElement("input");
    blacklistButton.type = "button";
    blacklistControl.appendChild(blacklistButton);
    card.qOne("a").prepend(blacklistControl);
    let creatorKey = `${card.getDataAttr("service")}-${card.getDataAttr("user")}`;
    if (BLACKLIST.includes(creatorKey)) {
      hiddenPosts.creatorBlacklisted++;
      card.addKfClass("blacklisted");
      if (!hiddenPosts.blacklistedCreators.includes(creatorKey))
        hiddenPosts.blacklistedCreators.push(creatorKey);
      blacklistButton.value = "Remove from Blacklist";
      blacklistButton.addEventListener("click", unblacklistCreator);

    } else {
      blacklistButton.value = "Add to Blacklist";
      blacklistButton.addEventListener("click", blacklistCreator);
    }

    let postKey = `${creatorKey}-${card.getDataAttr("id")}`;
    if (shownPosts.includes(postKey)) {
      hiddenPosts.alreadyShown++;
      card.addKfClass("already_shown");
    } else {
      shownPosts.push(postKey);
    }

    newPostCardContainer.appendChild(card);
  }
  sessionStorage.setItem("lastOffset", currentOffset);
  sessionStorage.setItem("shownPosts", JSON.stringify(shownPosts));

  // reset all page buttons active state
  let pageButtons = qAll("menu > a");
  if (pageButtons) for (const button of pageButtons) {
    button.blur();
  }

  // add messages about hidden posts
  let srcMessage = qOne("#paginator-top small");
  if (srcMessage) {
    let messageContainer = createKfElement("div", "postMessageContainer");
    srcMessage.after(messageContainer);

    if (hiddenPosts.noThumbnail > 0) {
      let message;
      if (hiddenPosts.noThumbnail > 1) {
        message = `Hid ${hiddenPosts.noThumbnail} posts with no thumbnail.`;
      } else {
        message = "Hid 1 post with no thumbnail.";
      }
      let messageNoThumbnail = createKfElement("small", "no_thumbnail_msg", message);
      messageNoThumbnail.addEventListener("click", showPostsWithNoThumbnail);
      messageContainer.appendChild(messageNoThumbnail);
    }

    if (hiddenPosts.creatorBlacklisted > 0) {
      let message;
      if (hiddenPosts.creatorBlacklisted > 1) {
        message = `Hid ${hiddenPosts.creatorBlacklisted} posts from `;
        if (hiddenPosts.blacklistedCreators.length > 1) {
          message += `${hiddenPosts.blacklistedCreators.length} blacklisted creator.`;
        } else {
          message += "a blacklisted creator."
        }
      } else {
        message = "hid 1 post from a blacklisted creator."
      }
      let messageBlacklisted = createKfElement("small", "blacklisted_msg", message);
      messageBlacklisted.addEventListener("click", showPostsFromBlacklistedCreators);
      messageContainer.appendChild(messageBlacklisted);
    }

    if (hiddenPosts.alreadyShown > 0) {
      let message;
      if (hiddenPosts.alreadyShown > 1) {
        message = `Hid ${hiddenPosts.alreadyShown} posts already shown on previous pages.`;
      } else {
        message = "Hid 1 post already shown on previous pages.";
      }
      let messageAlreadyShown = createKfElement("small", "already_shown_msg", message);
      messageAlreadyShown.addEventListener("click", showPostsFromPreviousPages);
      messageContainer.appendChild(messageAlreadyShown);
    }

    updateStyles();
  }
  newPostCardContainer.setKfAttr("href", window.location.href);
  srcPostCardContainer.after(newPostCardContainer);
  srcPostCardContainer.style.display = "none";
}

function fixThumbnailAspectRatio(event) {
  let img = event.target;
  let aspectRatio = img.naturalWidth / img.naturalHeight;
  if (OPTIONS.adapt_post_card_height_to_thumbnail) {
    img.closest("article").style.aspectRatio = Math.min(aspectRatio, 1);
    if (aspectRatio - 1 > 0.167) img.style.objectFit = "contain";
  } else {
    if (aspectRatio - 2/3 > 0.167) img.style.objectFit = "contain";
  }
  img.style.opacity = 1;
}

function showPostsWithNoThumbnail(event) {
  removeStyle("hide_no_thumbnail");
}

function showPostsFromBlacklistedCreators(event) {
  removeStyle("hide_blacklisted");
}

function showPostsFromPreviousPages(event) {
  removeStyle("hide_already_shown");
}

function preventDefault(event) {
  event.preventDefault();
}

function blacklistCreator(event) {
  event.preventDefault();
  let card = event.target.closest("article");
  let creatorKey = `${card.getDataAttr("service")}-${card.getDataAttr("user")}`;
  let message = `Do you want to blacklist ${creatorKey}?`;
  let title = card.qOne("header").childNodes[0];
  if (title) {
    message += `\n(Creator of post ${title.textContent})`;
  }
  if (confirm(message)) {
    addToBlacklist(creatorKey, title ? title.textContent : "");
  }
}

function addToBlacklist(creatorKey, comment) {
  BLACKLIST_RAW[creatorKey] = comment;
  GM_setValue("BLACKLIST", JSON.stringify(BLACKLIST_RAW));
  alert(`${creatorKey} added to blacklist, reload to take effect.`);
}

function unblacklistCreator(event) {
  event.preventDefault();
  let card = event.target.closest("article");
  let creatorKey = `${card.getDataAttr("service")}-${card.getDataAttr("user")}`;
  let message = `Do you want to unblacklist ${creatorKey}?`;
  let title = card.qOne("header").childNodes[0];
  if (title) {
    message += `\n(Creator of post ${title.textContent})`;
  }
  if (confirm(message)) {
    removeFromBlacklist(creatorKey);
  }
}

function removeFromBlacklist(creatorKey) {
  delete BLACKLIST_RAW[creatorKey];
  GM_setValue("BLACKLIST", JSON.stringify(BLACKLIST_RAW));
  alert(`${creatorKey} removed from blacklist, reload to take effect.`);
}
// ==</Page Fixing Functions>==

// ==<CSS>==
const CSS = {};
CSS.posts = `#kf-postCardContainer {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 5px;
  padding: 0 5px;
  align-items: end;
  justify-content: center;
}
.card-list {
  container-type: inline-size;
  container-name: postCardContainerParent;
}
@container postCardContainerParent (min-width: 1845px) {
  #kf-postCardContainer {
    grid-template-columns: repeat(10, 1fr);
  }
}
#kf-postCardContainer article {
  width: auto;
  height: auto;
  aspect-ratio: 2/3;
  filter: drop-shadow(0 2px 2px #000);
}
article a {
  border: 0;
}
.post-card__image-container {
  overflow: hidden;
}
.post-card__image {
  opacity: 0;
  transition: opacity 0.5s ease-out, filter 0.5s ease-out;
}
.kf-no_thumbnail footer::before {
  content: "No Thumbnail";
}
.kf-blacklisted .post-card__image {
  filter: blur(10px);
}
.kf-blacklisted .post-card__image:hover {
  filter: blur(0);
}
.kf-already_shown {
  opacity: .5;
}
#kf-no_thumbnail_msg,
#kf-blacklisted_msg,
#kf-already_shown_msg {
  display: none;
}
#kf-blacklist_control {
  z-index: 99;
  position: absolute;
  width: 100%;
  height: 100%;
  display: none;
  justify-content: center;
  align-items: center;
  cursor: default;
}
#kf-blacklist_control input {
  border: none;
  border-radius: 10px;
  padding: 5px;
  font-size: 15px;
  background: #9c2121;
  color: white;
  font-weight: bold;
}
#kf-blacklist_control input:hover {
  background: #d94a4a;
  cursor: pointer;
}
#kf-blacklist_control input[value="Remove from Blacklist"] {
  background: #47d5a6;
}
#kf-blacklist_control input[value="Remove from Blacklist"]:hover {
  background: #9ae8ce;
}

`;
if (OPTIONS.adapt_post_card_height_to_thumbnail) CSS.posts += `
#kf-postCardContainer article {
  aspect-ratio: 1;
  transition: aspect-ratio 0.5s ease-out;
}
#kf-postCardContainer .kf-no_thumbnail {
  aspect-ratio: unset;
  height: fit-content;
}`;
CSS.hideNoThumbnail = `.kf-no_thumbnail { display: none; }
#kf-no_thumbnail_msg { display: block !important; }`;
CSS.hideBlacklisted = `.kf-blacklisted { display: none; }
#kf-blacklisted_msg { display: block !important; }`;
CSS.hideAlreadyShown = `.kf-already_shown { display: none; }
#kf-already_shown_msg { display: block !important; }`;
CSS.blacklistMode = `.kf-blacklisted .post-card__image { filter: blur(0) !important; }
#kf-blacklist_control { display: flex !important; }`;
// ==</CSS>==
})();