Kemono Fixer

Allow you to blacklist creators on Kemono, and more.

2025-10-05 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

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 or Violentmonkey 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        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>==
})();