post #9000000 GET!
Donmai

Danbooru (etc...) userscripts

Posted under General

Easier 1up b37

Forked from https://gist.github.com/TypeA2/bff1474c0f4ca2188cf21897d4e4b2dd

This is a userscript written for users below level 37. Additionally, it allows you to 1up related posts from the same source.

Show
// ==UserScript==
// @name        Easier 1up b37
// @namespace   https://danbooru.donmai.us/forum_topics/8502
// @match       *://*.donmai.us/uploads/*
// @grant       none
// @version     0.0.2-fork
// @author      Sibyl
// @description 1.0.3 2024-06-19 (upstream), 2024-12-31 (current)
//              Forked from https://gist.github.com/TypeA2/bff1474c0f4ca2188cf21897d4e4b2dd
// @run-at      document-end
// ==/UserScript==

const CUSTOM_THUMBNAIL = 23609685; // Custom thumbnail for banned posts with media asset ID

const easier1Up = {
  iconHash: document.querySelector("a#close-notice-link use").href.baseVal.split(/-|\./)[1],
  tagsField: document.querySelector("#post_tag_string"),
  async init() {
    let thumbnailData = JSON.parse(localStorage.getItem("easier_1up_b37"));
    if (!thumbnailData || thumbnailData.id !== CUSTOM_THUMBNAIL) {
      let resp = await (await fetch(`/media_assets/${CUSTOM_THUMBNAIL}.json`)).json();
      if (!resp || resp.error) {
        thumbnailData = {
          id: 23609685,
          url: "https://cdn.donmai.us/180x180/3e/3c/3e3c7baac2a12a0936ba1f62a46a3478.jpg",
          width: 180,
          height: 135
        };
      } else {
        thumbnailData = resp.variants.filter(v => v.type === "180x180")[0];
        thumbnailData.id = resp.id;
      }
      localStorage.setItem("easier_1up_b37", JSON.stringify(thumbnailData));
    }
    this.thumbnail = thumbnailData;

    const relatedPosts = document.querySelector("#related-posts-by-source p.fineprint a");
    if (relatedPosts) {
      const shownCount = Number(relatedPosts.innerText.split(" ")[0]);
      let articles = document.querySelectorAll("#related-posts-by-source article");
      const addButton = articles =>
        articles.forEach(el => {
          const div = document.createElement("div");
          this.addButton(el, div);
          el.querySelector(".post-preview-container").nextElementSibling.appendChild(div);
        });
      if ((articles.length === 5 && shownCount > 5) || articles.length === shownCount) addButton(articles);
      else {
        const url = new URL(relatedPosts.href);
        url.pathname = "/posts.json";
        url.searchParams.append("limit", 5);
        fetch(url)
          .then(resp => resp.json())
          .then(json => {
            articles = this.updateArticles(json, articles, true);
            addButton(articles);
          });
      }
    }

    const similar = document.getElementById("iqdb-similar");
    this.observer = new MutationObserver(ms => ms.forEach(m => m.addedNodes.forEach(this.process.bind(this))));
    this.observer.observe(similar, {
      subtree: true,
      childList: true
    });
  },
  async process(node) {
    if (node.className !== "iqdb-posts") return;
    let articles = node.querySelectorAll("#iqdb-similar article");
    let shownCount = articles.length;
    let iqdbNoPostFound = shownCount === 0 && document.querySelector(".post-gallery-grid > p:only-child");
    if (!iqdbNoPostFound && shownCount !== 5) {
      let iqdbResults = await this.iqdbReq();
      if (iqdbResults.length !== shownCount) articles = this.updateArticles(iqdbResults, articles);
    }
    for (const post of articles) {
      const div = post.querySelector(".iqdb-similarity-score").parentElement;
      this.addButton(post, div);
    }
    this.observer?.disconnect();
  },
  copyTags(post, isParent) {
    const tags = post.dataset.tags.split(" ").filter(t => t === "social_commentary" || t.indexOf("commentary") == -1);
    document.querySelector(`input.radio_buttons[value='${post.dataset.rating}']`).checked = true;
    if (isParent) {
      document.getElementById("post_parent_id").value = post.dataset.id;
    } else tags.push("child:" + post.dataset.id);
    this.tagsField.value = tags.join(" ") + " ";
    this.tagsField.dispatchEvent(new InputEvent("input", { bubbles: true }));
    document.querySelector(".source-tab").click();
    Danbooru.Utility.notice("Successfully copied tags. Please check the commentary tags.");
  },
  addButton(post, div) {
    const setParent = document.createElement("a");
    setParent.classList.add("inactive-link");
    setParent.href = "#";
    setParent.innerText = "parent";
    setParent.addEventListener("click", e => {
      e.preventDefault();
      this.copyTags(post, true);
    });
    const setChild = document.createElement("a");
    setChild.classList.add("inactive-link");
    setChild.href = "#";
    setChild.innerText = "child";
    setChild.addEventListener("click", e => {
      e.preventDefault();
      this.copyTags(post, false);
    });
    div.children.length && div.appendChild(document.createTextNode(" | "));
    div.appendChild(setParent);
    div.appendChild(document.createTextNode(" | "));
    div.appendChild(setChild);
  },
  async iqdbReq() {
    try {
      let mid = document.getElementById("media_asset_id").value;
      let resp = await (
        await fetch(`/iqdb_queries.json?limit=5&search%5Bmedia_asset_id%5D=${mid}&search%5Bsimilarity%5D=50&search%5Bhigh_similarity%5D=70`)
      ).json();
      if (Array.isArray(resp)) return resp;
      else throw new Error(JSON.stringify(resp));
    } catch (e) {
      console.error("Error:", e);
    }
  },
  updateArticles(posts, currentPosts, relatedSection = false) {
    currentPosts = Array.from(currentPosts);
    const currentPostIds = currentPosts.map(el => {
      return Number(el.getAttribute("data-id"));
    });
    currentPostIds.push(0);
    let idx = 0,
      postsLength = posts.length;
    currentPostIds.forEach((pid, index) => {
      let htmlToInsert = "";
      for (; idx < postsLength; idx++) {
        let post = relatedSection ? posts[idx] : posts[idx].post;
        if (post.id !== pid) {
          if (post.is_banned) htmlToInsert += relatedSection ? this.render(post) : this.render(post, posts[idx].score);
        } else break;
      }
      if (htmlToInsert) {
        if (pid === 0) {
          const prefix = relatedSection ? "#related-posts-by-source" : ".iqdb-posts";
          document.querySelector(prefix + " .posts-container").insertAdjacentHTML("beforeend", htmlToInsert);
        } else currentPosts[index].insertAdjacentHTML("beforebegin", htmlToInsert);
      }
    });
    const prefix = relatedSection ? "#related-posts-by-source" : "#iqdb-similar";
    return document.querySelectorAll(prefix + " article");
  },
  render(
    {
      id,
      uploader_id,
      score,
      rating,
      tag_string,
      is_pending,
      is_flagged,
      is_deleted,
      has_children,
      parent_id,
      source,
      media_asset: { id: mid, image_width, image_height, file_size, file_ext }
    },
    similarity
  ) {
    const dataFlag = is_pending ? "pending" : is_flagged ? "flagged" : is_deleted ? "deleted" : "";

    const classList = ["post-preview", "post-preview-fit-compact", "post-preview-180"];
    is_pending && classList.push("post-status-pending");
    is_flagged && classList.push("post-status-flagged");
    is_deleted && classList.push("post-status-deleted");
    has_children && classList.push("post-status-has-children");
    parent_id && classList.push("post-status-has-parent");
    similarity && classList.push(similarity < 70 ? "iqdb-low-similarity hidden" : "iqdb-high-similarity");

    const { url, width, height } = this.thumbnail;
    const similarityHtml = similarity
      ? `<div><a class="inactive-link iqdb-similarity-score" href="/iqdb_queries?post_id=${id}">${similarity.toFixed(0)}% similar</a></div>`
      : "";

    return `<article id="post_${id}" class="${classList.join(
      " "
    )}" data-id="${id}" data-tags="${tag_string}" data-rating="${rating}" data-flags="${dataFlag}" data-score="${score}" data-uploader-id="${uploader_id}">
<div class="post-preview-container">
<a class="post-preview-link" draggable="false" href="/posts/${id}">
<picture><img src="${url}" width="${width}" height="${height}" class="post-preview-image" title="" alt="post #${id}"draggable="false" aria-expanded="false" data-title="${tag_string} rating:${rating} score:${score}"></picture>
</a></div><div class="text-xs text-center mt-1"><div>
<a rel="external noreferrer nofollow" title="${source}" class="inline-block align-top" href="${source}">
<svg class="icon svg-icon globe-icon h-4" viewBox="0 0 512 512"><use fill="currentColor" href="/packs/static/icons-${this.iconHash}.svg#globe"></use></svg>
</a>
<a href="/media_assets/${mid}">${this.formatBytes(file_size)} .${file_ext}, ${image_width}×${image_height}</a></div>${similarityHtml}</div>
</article>`;
  },
  formatBytes(bytes) {
    if (bytes === 0) return "0 Bytes";
    const units = ["Bytes", "KB", "MB"];
    const k = 1024;
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    const value = bytes / Math.pow(k, i);
    const formattedValue = value % 1 === 0 ? value.toFixed(0) : value.toFixed(2);
    return `${formattedValue} ${units[i]}`;
  }
};
easier1Up.init();

Get a better experience by using it with the script in forum #312212.

Updated

SpikySquid said:

Easy Pre-tagging manager

  • "Save tags" button on the upload page for saving tags for unposted uploads
  • Posts => Uploads => Pre-tagged Uploads - Browse pre-tagged uploads
  • The userscript saves all of the data locally on your browser's config folder by using JavaScript's localStorage variable
Show
// ==UserScript==
// @name         Danbooru - Easy Pre-tagging manager
// @namespace    http://tampermonkey.net/
// @version      2024-10-29
// @description  Save and manage tags on unposted uploads
// @author       yyk
// @match        https://danbooru.donmai.us/*
// @match        https://aibooru.online/*
// @match        https://gaybooru.app/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=donmai.us
// @grant        none
// ==/UserScript==

// Before using the plugin type:
// localStorage.setItem("danbooru_savedtags_json", `[]`);
// into your JavaScript console
// created at 2024-09-02

(function() {
    'use strict';

    if(!JSON.parse(localStorage.getItem(`danbooru_savedtags_json_(${window.location.host})`))) {localStorage.setItem(`danbooru_savedtags_json_(${window.location.host})`, `[]`);}

    if(window.location.href.startsWith(`https://${window.location.host}/uploads/new`) || /\/users\/\d+\/uploads/.test(window.location.href) || /\/uploads\/\d+\/assets/.test(window.location.href) ) {
        document.querySelector("#subnav-all-uploads").outerHTML+=`
        <li id="subnav-all-uploads"><a id="subnav-all-uploads-link" href="/pretagged_uploads">Pre-tagged uploads</a></li>
        `
    }

    if(  /\/users\/\d+\/uploads/.test(window.location.href) || /\/uploads\/\d+\/assets/.test(window.location.href) ) {
        var danbooru_savedtags_json=JSON.parse(localStorage.getItem(`danbooru_savedtags_json_(${window.location.host})`));
        (document.querySelectorAll(".media-asset-preview a")).forEach(item => {
            var existing_upload=danbooru_savedtags_json.find((element) => element.loc == `${item.href.replace(`https://${window.location.host}/uploads/`,"")}`);
            //console.log( existing_upload===undefined )
            if(existing_upload!=undefined) {
                item.style.display="relative";
                item.innerHTML+="<div style='overflow:hidden; color:white; background:green; opacity:0.7; width:100%; height:15px; bottom:0; right:0; position:absolute; font-size:14px'>Pretagged</div>"
            }
            else if(item.href.endsWith("assets")) {
                var existing_upload=danbooru_savedtags_json.find((element) => element.loc.split("/")[0] == item.href.split("/")[4]);
                if(existing_upload!=undefined) {
                    item.style.display="relative";
                    item.innerHTML+="<div style='overflow:hidden; color:white; background:green; opacity:0.7; width:100%; height:15px; bottom:0; right:0; position:absolute; font-size:14px'>Contains pretagged</div>"
                }
            }

        })
    }

    if(window.location.href.startsWith(`https://${window.location.host}/pretagged_uploads`)) {
        var myFunctions = window.myFunctions = {};
        document.title="DB | Pretagged uploads"

        myFunctions.pasteDBSTJ = function() {
            danbooru_savedtags_json=JSON.parse(localStorage.getItem(`danbooru_savedtags_json_(${window.location.host})`));
            console.log(
                JSON.stringify(danbooru_savedtags_json).split("").reverse().join("")
            )
        }

        myFunctions.removePretagsFor = function(locix) {
            console.log(locix);
            if (confirm(`Are you sure about deleting your saved tags for ${locix}? Only delete saved tags if the asset was already uploaded or is a duplicate.`) == true) {
                var existing_upload=danbooru_savedtags_json.find((element) => element.loc == `${locix}`);
                let index = danbooru_savedtags_json.indexOf(existing_upload);
                if(index!=-1) {
                    danbooru_savedtags_json.splice(index, 1);
                    localStorage.setItem(`danbooru_savedtags_json_(${window.location.host})`, JSON.stringify(danbooru_savedtags_json));
                }
                document.querySelector(`#pretag-item-${locix.replaceAll("/","-")}`).remove();
            } else {
                text = "You canceled!";
            }
        }

        myFunctions.getColorFromScore = function(score) {
            var maxVal=30;
            score = Math.max(0, Math.min(maxVal, score));

            const percentage = (score / maxVal) * 100;

            const red = Math.floor(255 - (percentage / 100) * 255);
            const green = Math.floor((percentage / 100) * 255);

            const hex = `#${red.toString(16).padStart(2, '0')}${green.toString(16).padStart(2, '0')}00`;

            return hex;
        }

        var danbooru_savedtags_json=JSON.parse(localStorage.getItem(`danbooru_savedtags_json_(${window.location.host})`));
        document.querySelector("#a-not-found").innerHTML=`
           <div id="pretagged-posts" class="user-favorites recent-posts">
   <h2 onclick="myFunctions.pasteDBSTJ()" class="recent-posts-header">Pre-tagged posts</h2>`;

        var timeout_slp=50;
        danbooru_savedtags_json=danbooru_savedtags_json.sort((a, b) => b.loc.localeCompare(a.loc));
        danbooru_savedtags_json.forEach(item => {
              var tag_count=item.tagstr.replaceAll("\n"," ").split(" ").length;
              var tag_count_bg=myFunctions.getColorFromScore(tag_count);
              document.querySelector("#pretagged-posts").innerHTML+=`<div id="pretag-item-${item.loc.replaceAll("/","-")}" style="width:150px; height:190px; float:left; margin:3px; position:relative;">
              <div onclick="myFunctions.removePretagsFor('${item.loc}')" class="delete-butt" style="background-color:red; border-radius:3px; width:25px; height:25px; position:absolute; right:0; opacity:0.5; z-index:90"></div>
      <a class="post-preview-link" draggable="false" href="/uploads/${item.loc}">
         <picture>
            <img src="https://cdn.donmai.us/180x180/41/55/41553d2ffa946482b91fb0cd51bc59ab.jpg" style="width:145px;height:145px;object-fit:contain;" class="post-preview-image">
         </picture>
      </a>

      <div class="post-preview-score text-sm text-center mt-1">
         <span onclick="console.log(\`${item.tagstr}\`)" class="post-score inline-block text-center whitespace-nowrap align-middle min-w-4">
           ${item.loc}</span><br>
           <span style="background: linear-gradient(rgba(0,0,0,0.4),rgba(0,0,0,0.4)), linear-gradient(${tag_count_bg},${tag_count_bg}); padding: 0px 4px 1px 4px; border-radius:2px; border:1px solid white;">${tag_count} tags</span>
      </div>

   </div>

  </div>`;

            setTimeout(function() {
            fetch(`/uploads/${item.loc}.json`,{
                credentials: "same-origin"
            })
            .then(response => response.json())
            .then(
                json => {
                    //console.log(JSON.stringify(json))
                    if(item.loc.includes("/assets/")) {
                         fetch(`/media_assets/${json.media_asset_id}.json`,{
                             credentials: "same-origin"
                         })
                         .then(response => response.json())
                         .then(json2 => {
                             //console.log(JSON.stringify(json2))
                             document.querySelector(`#pretag-item-${item.loc.replaceAll("/","-")} img`).src=json2.variants[0].url;
                         })
                    }
                    else {
                         document.querySelector(`#pretag-item-${item.loc.replaceAll("/","-")} img`).src=json.upload_media_assets[0].media_asset.variants[0].url;
                    }
                }
            )
            }, timeout_slp);
            timeout_slp+=200;

        })
    }

    if(/(\/uploads\/\d+$)|(\/uploads\/\d+\/assets\/\d+$)/.test(window.location.href)) {
        var myFunctions = window.myFunctions = {};

        myFunctions.getUploadItemLocationPath = function() {
            return window.location.href.replace(`https://${window.location.host}/uploads/`,"").replaceAll(/\?.*/g,'');
        }

        myFunctions.loadSavedTagstr = function() {
            var upload_loc=myFunctions.getUploadItemLocationPath();
            var danbooru_savedtags_json=JSON.parse(localStorage.getItem(`danbooru_savedtags_json_(${window.location.host})`));
            var existing_upload=danbooru_savedtags_json.find((element) => element.loc == `${upload_loc}`);
            if(existing_upload!=undefined) {
                document.querySelector("#post_tag_string").value=existing_upload.tagstr;
            }
        }

        myFunctions.saveTagString = function() {
            var upload_loc=myFunctions.getUploadItemLocationPath();
            var danbooru_savedtags_json=JSON.parse(localStorage.getItem(`danbooru_savedtags_json_(${window.location.host})`));
            var existing_upload=danbooru_savedtags_json.find((element) => element.loc == `${upload_loc}`);
            console.log(upload_loc)
            if(existing_upload!=undefined) {
                existing_upload.tagstr=document.querySelector("#post_tag_string").value;
            }
            else {
                existing_upload={loc:`${upload_loc}`, tagstr:document.querySelector("#post_tag_string").value};
                danbooru_savedtags_json.push(existing_upload);
            }
            localStorage.setItem(`danbooru_savedtags_json_(${window.location.host})`, JSON.stringify(danbooru_savedtags_json));
            console.log(danbooru_savedtags_json);
        }

        myFunctions.updateGUI = function () { console.log("hi") };

        document.querySelector(".mb-4").outerHTML='<input type="submit" onclick="preventDefault();" id="btn-pass-tags" style="float: left; margin-right: 6px;" type="button" value="Save tags" class="button-primary button-sm">'+document.querySelector(".mb-4").outerHTML;
        document.querySelector("#btn-pass-tags").addEventListener("click", function(ev) {
            ev.preventDefault();
            myFunctions.saveTagString();
            console.log(myFunctions.getUploadItemLocationPath());
            localStorage.setItem("somecuterandomstring", myFunctions.getUploadItemLocationPath());
        });

        myFunctions.loadSavedTagstr();

        const queryString = window.location.search;
        const urlParams = new URLSearchParams(queryString);
        const pretagged_tags = urlParams.get('pretagged_tags')
        if(pretagged_tags!=null) {
            document.querySelector("#post_tag_string").value+=" "+pretagged_tags
        }

    }

})();

I modified this to allow it to work alongside "Validate Tag Input".

https://gist.github.com/Shinjidude/d8b815519a2a07ca82d25c66a213a06b/raw/DanbooruEasyPretaggingManager.user.js

Use source img to preview image when uploading

The image server is having hiccups right now, so I made this script to use image previews from the original source.

  • This will not work with assets that weren't uploaded by pasting the source link.
  • This will not work with pixiv images and it simply won't replace the img.src (image source).
  • This will not work if the image was deleted from it's original source or if the link is private or inaccessible.
Show
// ==UserScript==
// @name        Danbooru - Use source img to preview image when uploading
// @namespace   Violentmonkey Scripts
// @match       https://danbooru.donmai.us/uploads/*
// @grant       none
// @version     1.0
// @author      YYK
// @description 12/13/2024, 2:05:20 PM
// ==/UserScript==

(function() {

    fetch(window.location.href+".json")
    .then(response=>response.json())
    .then(data=>{
        console.log(data)
        var new_src_link=""
        if(data.upload_media_assets) new_src_link=(data.upload_media_assets[0]['source_url'])
        if(data.source_url) new_src_link=data.source_url
        img_link_valid=(new_src_link.includes("https://") && !new_src_link.includes("/i.pximg.net/"))
        if(img_link_valid) document.querySelector(".media-asset-component img").src=new_src_link
    });

})()

Updated

Probably not the right thread to ask this but I used to have a bookmarklet that would grab pixiv/bsky/twitter user IDs. I lost it when upgrading to a new version of Firefox and can't find it anymore. Is this one still around or does an userscript port of it exist?. Would love to have it again.

Asht said:

Probably not the right thread to ask this but I used to have a bookmarklet that would grab pixiv/bsky/twitter user IDs. I lost it when upgrading to a new version of Firefox and can't find it anymore. Is this one still around or does an userscript port of it exist?. Would love to have it again.

This one generally works. Copy the text starting from javascript:

I had plans for writing something like this for a bit but was a bit lazy. I saw UnderCurve metion it on discord so I threw something together. I don't see any glaring issues with the brief use so far so I thought I'd share it.

The script checks the artist of an upload for deleted posts/ai tagged art and presents a color accordingly.

Green - Nothing deleted
Yellow - More than 1 deleted image (on the first page)
Orange - More than half deleted images (on the first page)
Red - Post found with "ai-generated"

Code
// ==UserScript==
// @name         AI Check
// @namespace    http://tampermonkey.net/
// @version      2024-12-31
// @description  Spy check!
// @author       waterflame
// @match        https://danbooru.donmai.us/uploads/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=donmai.us
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    async function process() {
        const tag = document.querySelector("#post_tag_string").value;
        if (tag == "") return;
        document.querySelector("#aic").innerText = " - Loading...";
        const resp = await fetch(`https://danbooru.donmai.us/posts.json?tags=${encodeURIComponent(tag)}`, {
            method: 'get',
            headers: {
                'Content-Type': 'application/json'
            }
        });

        if (!resp.ok) {
            document.querySelector("#aic").innerText = ` - ${resp.status} ${resp.statusText}`;
            return;
        }

        const posts = await resp.json();
        var deleted = 0;
        var ai = 0;
        for (const post of posts) {
            if (post.is_deleted) {
                deleted += 1;
            }
            if (post.tag_string.includes("ai-generated")) {
                ai += 1;
            }
        }

        if (ai > 0) {
            document.querySelector("#aic").innerText = " - Red";
            document.querySelector("#aic").parentElement.style.color = "red";
        } else if (deleted >= (Math.round(posts.length/2))) {
            document.querySelector("#aic").innerText = ` - Orange(${deleted}/${posts.length})`;
            document.querySelector("#aic").parentElement.style.color = "orange";
        } else if (deleted > 0) {
            document.querySelector("#aic").innerText = ` - Yellow(${deleted}/${posts.length})`;
            document.querySelector("#aic").parentElement.style.color = "#dad55e";
        } else {
           document.querySelector("#aic").parentElement.style.color = "green";
           document.querySelector("#aic").innerText = " - Green";
        }
    }

    const span = document.createElement("span");
    span.innerText = "Artist Check";
    const s2 = document.createElement("span");
    s2.id = "aic";
    span.append(s2);
    document.querySelector("#related-tags-container").insertAdjacentElement("beforeBegin",span);
    document.querySelector("#related-tags-container").insertAdjacentHTML("beforeBegin","<br><br>");
    process();
})();
Vibrate on Vote and Favorite

Trigger vibration on supported devices when voting or favoriting.

This script does not work in browsers that do not support the vibration API (e.g. Firefox).

Show
// ==UserScript==
// @name         Vibrate on Vote and Favorite
// @namespace    https://danbooru.donmai.us/forum_topics/8502
// @version      1.1
// @author       Sibyl
// @run-at       document-end
// @match        *://*.donmai.us/*
// @grant        none
// ==/UserScript==

(() => {
  "use strict";

  let origOpen = window.XMLHttpRequest.prototype.open;
  window.XMLHttpRequest.prototype.open = function (method, url) {
    if (method === "POST" || method === "DELETE") {
      const patterns = [
        url => url.includes("/favorites?post_id="),
        url => /\/(comment_votes|favorites)\/\d+$/.test(url),
        url => /\/(comments|posts)\/\d+\/votes/.test(url),
        url => /\/forum_post_votes\/\d+\.js/.test(url),
        url => url.includes("/forum_post_votes.js?forum_post_id=")
      ];

      if (patterns.some(test => test(url))) {
        this.addEventListener("readystatechange", function () {
          if (this.readyState !== XMLHttpRequest.DONE) return;
          if (this.response.indexOf("Danbooru.Utility.error") === -1) {
            if (navigator.vibrate) navigator.vibrate(50);
            else Danbooru.Utility.error("Vibration API is not supported");
          }
        });
      }
    }
    return origOpen.apply(this, [].slice.call(arguments));
  };
})();

Updated

Snippet to quickly edit banned posts.
Requires the corresponding asset to be in your uploads.
Put in the browser console on any danbooru page, edit constants on top, execute.

Show
(async () => {
  // ID of the post to change
  const POST_ID = 1;
  // New additional tags/metatags (rating:, parent:, etc.)
  // `source:`, `-` (removing tags), some other metatags are not accepted
  const NEW_TAGS = ``;

  // ---------------------------------------------------------------------------
  const uploads = await $.get("/uploads.json", {
    only: "id,upload_media_assets[id,media_asset_id]",
    "search[upload_media_assets][post][id]": POST_ID,
  });
  const upload = uploads[0];
  if (upload === undefined) {
    throw new Error("No upload found");
  }
  const post = await $.get(`/posts/${POST_ID}.json`, {
    only: "id,media_asset[id],rating,parent_id",
  });
  const upload_media_asset = upload.upload_media_assets.filter(
    (uma) => uma.media_asset_id === post.media_asset.id
  )[0];

  await $.post("/posts.json", {
    upload_media_asset_id: upload_media_asset.id,
    post: {
      tag_string: NEW_TAGS,
      rating: post.rating,
      parent_id: post.parent_id,
    },
  });
  window.location = `/post_versions?search[post_id]=${POST_ID}`;
})();

Is there a script or method for highlighting non existent tags on upload to filter out typos and what not?

I tried messing around with this tag by @hdk5, by changing the (hard to see) dotted line to the color of the artist tag (I didn't actually care much for category highlighting I just wanted to filter out typos)

hdk5 said:

Input Tag Highlight

Adds highlighting to the post create/edit tag input field.
Highlights normal tags by category, crosses deprecated tags, underlines empty tags, highlights metatags name:"value".

Example: https://raw.githubusercontent.com/hdk5/danbooru.user.js/refs/heads/master/readme/input-tag-highlight.png
Install: https://github.com/hdk5/danbooru.user.js/blob/master/dist/input-tag-highlight.user.js?raw=1

(May be a bit janky, but is confirmed to work properly on desktop Firefox and desktop Edge.)

... but I am not actually a programmer and couldnt get it to work, though I could get the strikethrough to work.

There's also this by @BrokenEagle98 but it requires manually clicking the check button which is annoying and the above prooves "automatically" checking tags is possible.

BrokenEagle98 said:

ValidateTagInput checks all tag adds and removals on a post edit. For tag adds it validates that no new tags are being created via mistaggings or mispellings. For tag removes it checks the implication hierarchy to validate that no existing tags will readd that tag once it is removed.

Installation

Project page
Main script
Usage notes

There are two buttons: Submit and Check. Both will perform tag validation, but only submit will go on to submit the tags.

If one of the validations fail, it presents an error notice. For tag adds, it lists all of the new tags that will be created. For tag removes, it lists all of the hierarchy relationships that will cause a tag removal to be readded.

Additionally, it will display a Skip Validation checkbox below the Submit button. Selecting this will cause the userscript to ignore any validation failures and submit the tag changes as is.

One way to cause the tag validator to ignore a particular tag, especially if it is intentional, is to preface it with the metatag for its character type (example: general:this_is_a_new_tag).

Final

Any suggestions or feedback is appreciated.

Latest edits

  • (2022-09-04) Updated script link in OP

Versions

Show
  • (2017-09-24)
    • Version 2 - Initial release
    • Version 3 - Removed tagtype tags as a consideration for tag add validation <kittey: forum #136421>
  • (2017-09-25)
    • Version 4 - Added support for uploads page
    • Version 5 - Added remove tag validator
  • (2017-09-26)
    • Version 6
      • Added alias check to add tag validator
      • Normalize tags to lowercase <nonamethanks: forum #136522>
    • Version 7
      • Fixed a bug in data model, causing the submit process to hang
      • Restyled the warning messages
      • Saves all alias results regardless if an alias is found or not
  • (2017-09-27)
    • Version 8 - Rebound return key to use tag validator
  • (2017-09-28)
    • Version 9 - Fixed bug with final tag list calculation <nonamethanks: forum #136678>
  • (2017-09-29)
    • Version 10 - Negative tags can now be used to fix a prior mistake <nonamethanks: forum #136752>
    • Version 11 - Fixed bug with uploads page involving pre-edit tags <nonamethanks: Discord>
  • (2017-09-30)
    • Version 12 - Fixed timing bug with script <Unbreakable: forum #136728>
  • (2017-10-08)
    • Version 13 - Added check rating exists validator
  • (2017-10-10)
    • Version 14 - Changed data model and added data model validator
  • (2017-10-13)
    • Version 15 - Added tag validation to quick edit mode
  • (2017-10-16)
    • Version 16 - Added reset cached data link to user settings
  • (2017-10-21)
    • Version 17 - Migrated to IndexedDB for data storage
      • Uses Session Storage to mitigate DB penalty
    • Local Storage is used as a fallback for IndexedDB
      • It now gets pruned when it reaches a certain size
    • Fixed issue with quoted source metatag
  • (2017-10-22)
    • Version 18 - Made debug info conditional
      • Bug fix with pruning function
  • (2017-11-13)
    • Version 19 - Accounted for new Meta tag category (ref topic #14678)
  • (2017-11-18)
    • Version 20
      • Bug fix with data model check
      • Rating validator now checks for the rating in the tag box
  • (2017-11-19)
    • Version 21 - Bug fix on post index page for Member/Anonymous <evazion: Discord>
  • (2017-11-21)
    • Version 22 - Removed rating exists validator (issue #3362)
  • (2018-01-13)
    • Version 23 - Full validation of client data
  • (2018-06-04)
    • Version 24
      • Updated to use library
      • Major code refactor
      • Add a button that just checks instead of submitting
  • (2018-09-04)
    • Version 25 Add settings menu
  • (2019-01-29)
    • Version 26
      • Added additional validators (artist, copyright, general)
      • Added cache editor to menu
  • (2019-02-13)
    • Version 27 Updated to newest library
  • (2019-12-26)
    • Version 28 Updated library version
  • (2022-02-19)
    • Version 29 Updated library version
    • Added approval submit check (contributors only)

Zalza said:

[...]

I have, mostly for myself, made this script, which does the checking automatically and makes fixing it a bit easier: https://github.com/TypeA2/booru-scripts/blob/master/Awoobooru3.user.js

It also includes the ability to autocorrect some common typos. It mostly works (I use it for all my uploading), but beware that there's like one or two tags out there that autocorrect into something incorrect, and I didn't add an override so you have to temporarily disable the script if you come across those. I've come across those tags like 2 times since I've made it, so it's probably fine.

I am actually currently rewriting it into something more maintainable, but this older one remains functional.

ANON_TOKYO said:

I have, mostly for myself, made this script, which does the checking automatically and makes fixing it a bit easier: https://github.com/TypeA2/booru-scripts/blob/master/Awoobooru3.user.js

It also includes the ability to autocorrect some common typos. It mostly works (I use it for all my uploading), but beware that there's like one or two tags out there that autocorrect into something incorrect, and I didn't add an override so you have to temporarily disable the script if you come across those. I've come across those tags like 2 times since I've made it, so it's probably fine.

I am actually currently rewriting it into something more maintainable, but this older one remains functional.

Thanks.

Is it possible to set up those "typo" auto corrections yourself? Though it is a different booru I'm also familiar with the re621 script for e621 which is a huge scrip that, among many other things, allows users to set up their own "aliases" and is insanely good. But it is cumbersome constantly copypasting between hydrus, then e621 then danbo when uploading. For uploading between sites I have it set to auto correct a bunch of tags not used on danbooru to "bangs" which is deprecated and alias tags to their danbooru equilivant. For example:

gen*pokemon legendary_pokemon pokemon_(species) -> pokemon_(creature)
eeveelution *_body *_fur -> bangs

I tried asking the creator once about compatibility with other sites but the aliasing is just one fraction of what it does (and I can see in github it relies on a ton of e6 libraries) and the creator apperently is almost too busy to maintain it for e6 let alone port to danbo.

Zalza said:

Thanks.

Is it possible to set up those "typo" auto corrections yourself? Though it is a different booru I'm also familiar with the re621 script for e621 which is a huge scrip that, among many other things, allows users to set up their own "aliases" and is insanely good. But it is cumbersome constantly copypasting between hydrus, then e621 then danbo when uploading. For uploading between sites I have it set to auto correct a bunch of tags not used on danbooru to "bangs" which is deprecated and alias tags to their danbooru equilivant. For example:

gen*pokemon legendary_pokemon pokemon_(species) -> pokemon_(creature)
eeveelution *_body *_fur -> bangs

I tried asking the creator once about compatibility with other sites but the aliasing is just one fraction of what it does (and I can see in github it relies on a ton of e6 libraries) and the creator apperently is almost too busy to maintain it for e6 let alone port to danbo.

There's a menu item in the userscript manager's menu (for ViolentMonkey this involves clicking on the extension icon). It also should work fine, although I haven't extactly tested it extensively, but it worked for me. You can edit the default dictionary with it, and it allows for regex and textual replacements, and you could use it for custom aliases too. It also supports JSON import and export (the format should be pretty self-explanatory).

Zalza said:

There's also this by BrokenEagle98 but it requires manually clicking the check button which is annoying and the above prooves "automatically" checking tags is possible.

You don't have to click the "Check" button by the way. It also does the checking once you click the "Submit" button or doing a Ctrl+Enter. The "Check" button is provided so that one can do a validation during the progress of tagging and not waiting until the very end.

Zalza said:

I tried messing around with this tag by @hdk5, by changing the (hard to see) dotted line to the color of the artist tag (I didn't actually care much for category highlighting I just wanted to filter out typos)

I intentionally did not use a separate color for empty tags, but also made them easy to decorate with user-styles, e.g.:

.tag-highlight-highlights .tag-highlight-empty {
    color: var(--red-6) !important;
}
More Tooltips

Show tooltips when hovering over artist, media asset or favgroup links.

Show
// ==UserScript==
// @name        More Tooltips
// @namespace   https://danbooru.donmai.us/forum_topics/8502
// @match       *://danbooru.donmai.us/*
// @grant       none
// @version     0.7
// @author      Sibyl
// @run-at      document-end
// @description Show tooltips when hovering over artist, media asset or favgroup links.
// ==/UserScript==

// Media assets preview settings
const ASSET_PREVIEW_SIZE = 360; // 180, 720
const DISPLAY_IMAGE_HEIGHT = 240;

document.head.insertAdjacentHTML(
  "beforeend",
  `<style>.stt-bubble,.stt-bubble>.stt-arrow{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.stt-bubble{--stt-bgcolor:var(--post-tooltip-background-color);--stt-title-bgcolor:var(--post-tooltip-header-background-color);--stt-arrow-color:var(--stt-bgcolor);background:var(--stt-bgcolor);border:1px solid var(--post-tooltip-border-color);position:absolute;text-align:center;border-radius:4px;z-index:9999;box-shadow:var(--shadow-lg)}.stt-style{cursor:help;border-bottom:1px dotted}.stt-bubble .stt-title{background:var(--stt-title-bgcolor);font-size:10px;border-radius:3px 3px 0 0}.stt-content{word-wrap:break-word;padding:.5em}.stt-bubble>.stt-arrow{position:absolute;border-width:0;pointer-events:none;left:50%;margin-left:0}.stt-bubble>.stt-arrow::after,.stt-bubble>.stt-arrow::before{content:'';position:absolute;left:0;border-style:solid;border-color:transparent}.stt-bubble.top>.stt-arrow{top:100%}.stt-bubble.top>.stt-arrow::before{top:0;border-width:7px 7px 0;border-top-color:var(--stt-arrow-color)}.stt-bubble.top>.stt-arrow::after{top:1px;border-width:7px 7px 0;border-top-color:var(--post-tooltip-border-color);z-index:-1}.stt-bubble.bottom>.stt-arrow{bottom:100%}.stt-bubble.bottom>.stt-arrow::before{bottom:0;border-width:0 7px 7px;border-bottom-color:var(--stt-arrow-color)}.stt-bubble.bottom>.stt-arrow::after{bottom:1px;border-width:0 7px 7px;border-bottom-color:var(--post-tooltip-border-color);z-index:-1}` +
    `.stt-content>div.artist-info{display:flex;flex-direction:column}.stt-content>div.artist-info>ul{max-height:240px;padding-right:.2rem;margin-bottom:.3rem;text-align:left}.stt-content>div.artist-info li{line-height:1.5;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.stt-bubble div.artist-info>p:last-of-type{display:inline-block;text-align:left;margin:0}.stt-content>div.artist-info>p>span:last-of-type{float:right;margin-right:.3rem;color:var(--muted-text-color)}` +
    `table.stt-favgroup thead tr{border-bottom:2px solid var(--table-header-border-color)}table.stt-favgroup tbody tr:hover{background:var(--table-row-hover-background)}table.stt-favgroup tbody tr{border-bottom:1px solid var(--table-row-border-color)}table.stt-favgroup tr:nth-child(2n){background:var(--table-even-row-background)}table.stt-favgroup td,table.stt-favgroup th{line-height:1.25}table.stt-favgroup th{text-align:center}table.stt-favgroup td,table.stt-favgroup th:first-child{text-align:left}</style>`
);

// Simple Tooltip - Forked from [tipso](https://github.com/object505/tipso) v1.0.8
// prettier-ignore
(t=>{const e="stt",s={background:null,titleBackground:null,titleContent:"",width:200,content:null,ajaxContentUrl:null,ajaxContentBuffer:0,contentElementId:null,useTitle:!1,templateEngineFunc:null,onBeforeShow:null,onShow:null,onHide:null};class i{constructor(e,i){this.element=e,this.$element=t(this.element),this.doc=t(document),this.win=t(window),this.settings={...s,...i,...this.$element.data("stt")},this._title=this.$element.attr("title"),this.mode="hide",this.init()}init(){const t=this.$element;t.addClass("stt-style").removeAttr("title");let s=null,i=null;t.on(`mouseover.${e}`,(t=>{t.ctrlKey||t.altKey||(clearTimeout(s),clearTimeout(i),i=setTimeout((()=>this.show()),150))})).on(`mouseout.${e}`,(()=>{clearTimeout(s),clearTimeout(i),s=setTimeout((()=>this.hide()),200),this.tooltip().on(`mouseover.${e}`,(()=>{this.mode="tooltipHover"})).on(`mouseout.${e}`,(()=>{this.mode="show",clearTimeout(s),s=setTimeout((()=>this.hide()),200)}))})),this.settings.ajaxContentUrl&&(this.ajaxContent=null)}tooltip(){return this.stt_bubble||(this.stt_bubble=t('<div class="stt-bubble"><div class="stt-title"></div><div class="stt-content"></div><div class="stt-arrow"></div></div>')),this.stt_bubble}show(){const t=this.tooltip(),s=this.win;if("hide"===this.mode){"function"==typeof this.settings.onBeforeShow&&this.settings.onBeforeShow(this.$element,this.element,this);const i=this.settings.width?{width:this.settings.width}:{width:200};t.css({"--stt-bgcolor":this.settings.background,"--stt-title-bgcolor":this.settings.titleBackground,...i}),t.find(".stt-content").html(this.content()),t.find(".stt-title").html(this.titleContent()),o(this),s.on(`resize.${e}`,(()=>o(this))),clearTimeout(this.timeout),this.timeout=setTimeout((()=>{t.appendTo("body").stop(!0,!0).fadeIn(400,(()=>{this.mode="show","function"==typeof this.settings.onShow&&this.settings.onShow(this.$element,this.element,this)}))}),200)}}hide(t=!1){const s=this.tooltip(),i=t?0:50;clearTimeout(this.timeout),this.timeout=setTimeout((()=>{"tooltipHover"!==this.mode&&s.stop(!0,!0).fadeOut(400,(()=>{s.remove(),"function"==typeof this.settings.onHide&&"show"===this.mode&&this.settings.onHide(this.$element,this.element,this),this.mode="hide",this.win.off(`resize.${e}`)}))}),i)}close(){this.hide(!0)}destroy(){this.$element.off(`.${e}`).removeData(e).removeClass("stt-style").attr("title",this._title),this.win.off(`resize.${e}`)}titleContent(){return this.settings.titleContent||this.$element.data("stt-title")}content(){let e;return this.settings.ajaxContentUrl?this._ajaxContent?e=this._ajaxContent:(e=t.ajax({type:"GET",url:this.settings.ajaxContentUrl,async:!1}).responseText,this.settings.ajaxContentBuffer>0?(this._ajaxContent=e,setTimeout((()=>{this._ajaxContent=null}),this.settings.ajaxContentBuffer)):this._ajaxContent=null):e=this.settings.contentElementId?t(`#${this.settings.contentElementId}`).text():this.settings.content?this.settings.content:this.settings.useTitle?this._title:this.$element.data("stt"),this.settings.templateEngineFunc&&(e=this.settings.templateEngineFunc(e,this)),e}update(t,e){if(!e)return this.settings[t];this.settings[t]=e}}function n(t){const e=t.clone().css("visibility","hidden").appendTo("body"),s=e.outerHeight(),i=e.outerWidth();return e.remove(),{width:i,height:s}}function o(e){const s=e.tooltip(),i=e.$element,o=t(window);let l=i.offset().left+i.outerWidth()/2-n(s).width/2,h=i.offset().top-n(s).height-10;const a=e.titleContent()?"var(--stt-title-bgcolor)":"var(--stt-bgcolor)";if(s.find(".stt-arrow").css({marginLeft:-7,marginTop:""}),h<o.scrollTop()?(h=i.offset().top+i.outerHeight()+10,s.css({"--stt-arrow-color":a}).removeClass("top bottom").addClass("bottom")):s.css({"--stt-arrow-color":"var(--stt-bgcolor)"}).removeClass("top bottom").addClass("top"),l<o.scrollLeft()&&(s.find(".stt-arrow").css({marginLeft:l-7}),l=10),l+e.settings.width>o.outerWidth()){const t=o.outerWidth()-(l+e.settings.width);s.find(".stt-arrow").css({marginLeft:-t-7,marginTop:""}),l+=t-10}s.css({left:l,top:h})}t.fn[e]=function(s){if("object"==typeof s||void 0===s)return this.each((function(){t.data(this,`plugin_${e}`)||t.data(this,`plugin_${e}`,new i(this,s))}));if("string"==typeof s&&"_"!==s[0]&&"init"!==s){let n;return this.each((function(){const o=t.data(this,`plugin_${e}`);o instanceof i&&"function"==typeof o[s]&&(n=o[s].apply(o,Array.prototype.slice.call(arguments,1))),"destroy"===s&&t.data(this,`plugin_${e}`,null)})),void 0!==n?n:this}}})(jQuery)

$(".tag-type-1 a, a.dtext-artist-id-link, a.dtext-wiki-link.tag-type-1").stt({
  width: 360,
  ajaxContentBuffer: 15e3,
  onBeforeShow: (_, el, instance) => {
    const url = new URL(el.href);
    const artist = url.searchParams.get("name") || url.searchParams.get("tags");
    if (artist && artist !== "banned_artist") {
      return instance.update("ajaxContentUrl", `/artists/show_or_new?name=${artist}`);
    } else if (url.pathname.startsWith("/artists/")) {
      const uid = url.pathname.slice(9);
      if (/^\d+$/.test(uid)) {
        return instance.update("ajaxContentUrl", `/artists/${uid}`);
      }
    }
    instance.destroy();
  },
  templateEngineFunc: content => {
    const doc = new DOMParser().parseFromString(content, "text/html");
    const uid = doc.body.dataset.artistId;
    const name = doc.querySelector("a.tag-type-1")?.innerText?.replace(/ /g, "_");
    const count = doc.querySelector("div#a-show span.post-count")?.innerText;
    let p = `<p><a target="_blank" href="/artists/${uid}/edit">Edit artist</a>&nbsp;|&nbsp;<a target="_blank" href="/post_versions?search%5Bchanged_tags%5D=${name}">Post Changes</a><span>`;
    if (count && count !== "0") {
      p += `<a target="_blank" class="inactive-link" href="/posts?tags=status%3Aany+${name}">${count}</a>,&nbsp;`;
    }
    p += `<a target="_blank" class="inactive-link" href="/artist_versions?search%5Bartist_id%5D=${uid}">0</a></span></p>`;
    const ul = doc.querySelector("div#a-show > *:not(.artist-wiki) ul:not(#blacklist-list)");
    if (ul) {
      ul.classList.add("thin-scrollbar", "text-xs");
      let lis = ul.children;
      let activeCount = Array.prototype.filter.call(lis, li => !li.children[1].classList.contains("inactive-artist-url")).length;
      ul.querySelectorAll("a").forEach(a => a.setAttribute("target", "_blank"));
      return `<div class="artist-info">${ul.outerHTML}${p.replace(">0</a></span>", `>${activeCount}/${lis.length}</a></span>`)}</div>`;
    } else if (!name) {
      const p = doc.querySelector("div#page>p").textContent;
      return `<div class="artist-info"><p class="m-0 py-1 text-sm" style="text-align:center"><i>${p}</i></p></div>`;
    } else return `<div class="artist-info"><p class="m-0 py-1 text-sm"><i>No URLs yet</i></p>${p}</div>`;
  }
});

$("a.dtext-media-asset-id-link").stt({
  width: "auto",
  ajaxContentBuffer: 15e3,
  onBeforeShow(_, el, instance) {
    const assetId = el.innerText.split("#")?.[1];
    if (assetId && /^\d+$/.test(assetId)) {
      instance.tooltip().css({ "min-width": "fit-content" });
      instance.tooltip().find(".stt-title").css({
        padding: "0 1rem",
        display: "flex",
        "flex-direction": "row",
        "justify-content": "center",
        "align-items": "center",
        gap: ".2rem",
        height: "1.3rem"
      });
      return instance.update("ajaxContentUrl", `/media_assets/${assetId}.json?only=md5,file_ext,file_size,image_width,image_height,duration,variants,post[id]`);
    }
    instance.destroy();
  },
  templateEngineFunc: (content, instance) => {
    let { md5, file_ext, file_size, image_width, image_height, duration, variants, post, error, message } = JSON.parse(content);
    file_size = formatBytes(file_size);
    duration = duration ? ` (${secondsToMinutes(duration)})` : "";
    let title = '<a target="_blank" class="inactive-link text-xs"';
    let meta = `${file_size} .${file_ext}, ${image_width}×${image_height}${duration}`;
    if (md5) {
      const url = variants.filter(s => s.type === "original")[0].url;
      title += ` href="${url}">${meta}`;
    } else title += error ? `>Error: ${error}` : `><s>${meta}</s>`;
    title += "</a>";
    const postId = post?.id;
    if (postId) {
      let src = md5
        ? "https://cdn.donmai.us/original/10/97/1097ebd471c28b70b4181f2dc1d44ca6.webp"
        : "https://cdn.donmai.us/original/69/3b/693ba3d904804b7e26ad1b0d831e64c9.png";
      title += `&nbsp;<a target="_blank" href="/posts/${postId}"><img class="icon h-3" src="${src}"></a>`;
    }
    instance.update("titleContent", title);
    if (!md5) return `<p class="m-0 py-1 text-sm"><i>${message || "Image unavailable."}</i></p>`;
    else {
      const { url, height } = variants.filter(s => s.type.startsWith(String(ASSET_PREVIEW_SIZE)))[0];
      return `<div class="stt-preview-container"><img style="height:${Math.min(DISPLAY_IMAGE_HEIGHT, height)}px" src="${url}"></div>`;
    }
  }
});

if (location.pathname.startsWith("/posts/")) {
  const postId = document.body?.dataset["postId"] || document.head.querySelector("meta[name='post-id']").getAttribute("content");
  fetch("/favorite_groups.json?only=id,name,creator&limit=100&search%5Bpost_ids_include_all%5D=" + postId)
    .then(resp => resp.json())
    .then(json => {
      if (Array.isArray(json)) {
        let len = json.length;
        len = len === 100 ? len + "+" : len;
        if (len !== 0)
          document
            .getElementById("post-info-favorites")
            ?.insertAdjacentHTML(
              "afterend",
              `<li id="post-info-favgroups">Favgroups: <a href="/favorite_groups?search%5Bpost_ids_include_all%5D=${postId}" target="_blank">${len}</a></li>`
            );
        $("li#post-info-favgroups > a").stt({
          width: "auto",
          content: json,
          ajaxContentBuffer: 15e3,
          templateEngineFunc: (content, instance) => {
            instance.tooltip().find(".stt-content").css({
              overflow: "hidden auto",
              "max-height": "240px",
              "overscroll-behavior-x": "contain",
              "scrollbar-width": "thin"
            });
            let html = '<table class="stt-favgroup text-xs"><thead><tr><th>Group</th><th>User</th></tr></thead><tbody>';
            for (let {
              id,
              name,
              creator: { id: uid, level_string, name: un, level }
            } of content) {
              html += `<tr><td><a href="/favorite_groups/${id}" target="_blank">${name}</a></td><td><a class="user user-${level_string.toLowerCase()}"  data-user-id="${uid}" data-user-name="${un}" data-user-level="${level}" href="/users/${uid}" target="_blank">${un}</a></td></tr>`;
            }
            html += "</tbody></table>";
            return html;
          }
        });
      }
    });
}

function formatBytes(bytes) {
  if (bytes === 0) return "0 Bytes";
  const units = ["Bytes", "KB", "MB"];
  const k = 1024;
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  const value = bytes / Math.pow(k, i);
  const formattedValue = value % 1 === 0 ? value.toFixed(0) : value.toFixed(2);
  return `${formattedValue} ${units[i]}`;
}

function secondsToMinutes(seconds) {
  const minutes = Math.floor(seconds / 60);
  const sec = seconds % 60;
  return `${String(minutes).padStart(2, "0")}:${String(sec).padStart(2, "0")}`.slice(0, 5);
}
Time Toggle

Toggle time elements between relative and absolute values. Simple but useful.

Show
// ==UserScript==
// @name         Time Toggle
// @namespace    https://danbooru.donmai.us/forum_topics/8502
// @version      1.0
// @description  Toggle time elements between relative and absolute values.
// @match        *://danbooru.donmai.us/*
// @run-at       document-end
// @grant        none
// ==/UserScript==

(function () {
  "use strict";

  document.addEventListener("click", event => {
    const target = event.target;
    if ((target.tagName === "TIME" && !target.closest("a")) || target.id === "post-info-date") {
      let el = target.tagName === "TIME" ? target : target.querySelector("time");
      let text = el.textContent;
      el.innerText = el.title;
      el.title = text;
    }
  });
})();

Updated

1 7 8 9 10 11