Donmai

Danbooru (etc...) userscripts

Posted under General

Automatically save content from upload page

This userscript can help you automatically save the content in textboxes on the upload page, including tags and translated commentary.

Show
// ==UserScript==
// @name         Auto Save Danbooru Upload Content
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  Automatically save content from upload page
// @author       Sibyl
// @match        *://*.donmai.us/*
// @grant        none
// ==/UserScript==

(async () => {
  "use strict";

  const pathname = location.pathname;
  const DB_STORE_NAME = "savedContentFromUploadPage";
  let assetId, db;
  if (
    pathname.startsWith("/posts/") &&
    !pathname.endsWith(".xml") &&
    !pathname.endsWith(".json")
  ) {
    assetId =
      document
        .querySelector("#related-tags-container")
        ?.getAttribute("data-media-asset-id") ||
      document
        .querySelector("#post-info-size > a[href^='/media_assets/']")
        ?.href.split("/media_assets/")[1];
    await openDB();
    remove(assetId);
  } else if (
    /^\/uploads\/\d+$/.test(pathname) ||
    /^\/uploads\/\d+\/assets\/\d+/.test(pathname)
  ) {
    assetId =
      document.querySelector("#media_asset_id")?.value ||
      document
        .querySelector("#related-tags-container")
        ?.getAttribute("data-media-asset-id");

    await openDB();

    const saved = await load(assetId);
    if (saved) {
      delete saved.asset_id;
      for (let elementName in saved) {
        document.querySelector(`#${elementName}`).value = saved[elementName];
      }
      document.querySelector("span.tag-count").innerText = "- / 20 tags";
    }

    document.addEventListener("input", event => {
      let el = event.target;
      switch (el.id) {
        case "post_tag_string":
        // case "post_source":
        // case "post_artist_commentary_title":
        // case "post_artist_commentary_desc":
        case "post_translated_commentary_title":
        case "post_translated_commentary_desc":
        case "post_parent_id":
          save({ [el.id]: el.value });
          break;
      }
    });

    const tagTextarea = document.querySelector("#post_tag_string");
    document
      .querySelector("#related-tags-container")
      .addEventListener("click", event => {
        const el = event.target;
        if (
          (el.tagName === "A" || el.tagName === "INPUT") &&
          el.closest("ul")?.className === "tag-list"
        ) {
          setTimeout(() => {
            const event = new Event("input", {
              bubbles: true,
              cancelable: true,
            });
            tagTextarea.dispatchEvent(event);
          });
        }
      });
  }

  function openDB() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open("AutoSavedDB", 1);

      request.onupgradeneeded = event => {
        db = event.target.result;
        if (!db.objectStoreNames.contains(DB_STORE_NAME)) {
          db.createObjectStore(DB_STORE_NAME, { keyPath: "asset_id" });
        }
      };

      request.onsuccess = event => {
        db = event.target.result;
        resolve();
      };

      request.onerror = event => reject(event.target.errorCode);
    });
  }

  function save(content) {
    const objectStore = db
      .transaction(DB_STORE_NAME, "readwrite")
      .objectStore(DB_STORE_NAME);
    const request = objectStore.get(assetId);
    request.onsuccess = event => {
      const updatedData = Object.assign(
        { asset_id: assetId },
        event.target.result,
        content
      );
      objectStore.put(updatedData);
    };
  }

  function load(assetId) {
    return new Promise((resolve, reject) => {
      const request = db
        .transaction(DB_STORE_NAME, "readonly")
        .objectStore(DB_STORE_NAME)
        .get(assetId);

      request.onsuccess = event => resolve(event.target.result);
      request.onerror = event => reject(event.target.errorCode);
    });
  }

  function remove(assetId) {
    db.transaction(DB_STORE_NAME, "readwrite")
      .objectStore(DB_STORE_NAME)
      .delete(assetId);
  }
})();

Use the following userscript if you want to avoid conflicts or overwriting when opening the same page at the same time. However, it doesn't work well on mobile devices.

Show
// ==UserScript==
// @name         Auto Save Danbooru Upload Content
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  Automatically save content from upload page
// @author       Sibyl
// @match        *://*.donmai.us/*
// @grant        none
// ==/UserScript==

(async () => {
  "use strict";

  const pathname = location.pathname;
  const DB_STORE_NAME = "savedContentFromUploadPage";
  let assetId, db;
  if (
    pathname.startsWith("/posts/") &&
    !pathname.endsWith(".xml") &&
    !pathname.endsWith(".json")
  ) {
    assetId =
      document
        .querySelector("#related-tags-container")
        ?.getAttribute("data-media-asset-id") ||
      document
        .querySelector("#post-info-size > a[href^='/media_assets/']")
        ?.href.split("/media_assets/")[1];
    await openDB();
    remove(assetId);
    clearEditingMark(assetId);
  } else if (
    /^\/uploads\/\d+$/.test(pathname) ||
    /^\/uploads\/\d+\/assets\/\d+/.test(pathname)
  ) {
    assetId =
      document.querySelector("#media_asset_id")?.value ||
      document
        .querySelector("#related-tags-container")
        ?.getAttribute("data-media-asset-id");

    if (checkForDuplicateEditing(assetId)) return;

    await openDB();
    markAsEditing(assetId);

    const saved = await load(assetId);
    if (saved) {
      delete saved.asset_id;
      for (let elementName in saved) {
        document.querySelector(`#${elementName}`).value = saved[elementName];
      }
      document.querySelector("span.tag-count").innerText = "- / 20 tags";
    }

    document.addEventListener("input", event => {
      let el = event.target;
      switch (el.id) {
        case "post_tag_string":
        // case "post_source":
        // case "post_artist_commentary_title":
        // case "post_artist_commentary_desc":
        case "post_translated_commentary_title":
        case "post_translated_commentary_desc":
        case "post_parent_id":
          save({ [el.id]: el.value });
          break;
      }
    });

    const tagTextarea = document.querySelector("#post_tag_string");
    document
      .querySelector("#related-tags-container")
      .addEventListener("click", event => {
        const el = event.target;
        if (
          (el.tagName === "A" || el.tagName === "INPUT") &&
          el.closest("ul")?.className === "tag-list"
        ) {
          setTimeout(() => {
            const event = new Event("input", {
              bubbles: true,
              cancelable: true,
            });
            tagTextarea.dispatchEvent(event);
          });
        }
      });

    window.addEventListener("beforeunload", () => {
      clearEditingMark(assetId);
    });
  }

  function openDB() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open("AutoSavedDB", 1);

      request.onupgradeneeded = event => {
        db = event.target.result;
        if (!db.objectStoreNames.contains(DB_STORE_NAME)) {
          db.createObjectStore(DB_STORE_NAME, { keyPath: "asset_id" });
        }
      };

      request.onsuccess = event => {
        db = event.target.result;
        resolve();
      };

      request.onerror = event => reject(event.target.errorCode);
    });
  }

  function save(content) {
    const objectStore = db
      .transaction(DB_STORE_NAME, "readwrite")
      .objectStore(DB_STORE_NAME);
    const request = objectStore.get(assetId);
    request.onsuccess = event => {
      const updatedData = Object.assign(
        { asset_id: assetId },
        event.target.result,
        content
      );
      objectStore.put(updatedData);
    };
  }

  function load(assetId) {
    return new Promise((resolve, reject) => {
      const request = db
        .transaction(DB_STORE_NAME, "readonly")
        .objectStore(DB_STORE_NAME)
        .get(assetId);

      request.onsuccess = event => resolve(event.target.result);
      request.onerror = event => reject(event.target.errorCode);
    });
  }

  function remove(assetId) {
    db.transaction(DB_STORE_NAME, "readwrite")
      .objectStore(DB_STORE_NAME)
      .delete(assetId);
  }

  function checkForDuplicateEditing(assetId) {
    const isEditing = localStorage.getItem(`editing_${assetId}`);
    if (isEditing && isEditing !== sessionStorage.getItem("editTimestamp")) {
      window.Danbooru.notice(
        "Same upload page has been opened in another tab. Edit in current page won't be saved."
      );
      return true;
    }
    return false;
  }

  function markAsEditing(assetId) {
    sessionStorage.setItem("editTimestamp", Date.now().toString());
    localStorage.setItem(
      `editing_${assetId}`,
      sessionStorage.getItem("editTimestamp")
    );
  }

  function clearEditingMark(assetId) {
    localStorage.removeItem(`editing_${assetId}`);
  }
})();

Updated

See deleted uploads percentage on profile

I'm not sure who authored this script but it's just a casual widget to attach some percentages to profiles. I know this doesn't mean much coming from me but please don't take any of the percentages as indicators for anything besides just that, percentages. It's floating around on Discord but never saw it posted here. It adds your total % of uploads deleted and the total % of post changes that are initial uploads.

Show
// ==UserScript==
// @name danbooru-deleted-uploads-percentage
// @match https://*.donmai.us/users/*
// @match https://donmai.moe/users/*
// @match https://*.donmai.us/profile
// @match https://donmai.moe/profile
// @grant none
// @version 1.1
// ==/UserScript==

function getElem(re) {
    return [...document.querySelectorAll("table.user-statistics th")].filter(th => th.innerText.match(re))[0].nextElementSibling;
}

const uploads = getElem(/^Uploads/),
      deleted = getElem(/^Deleted Uploads/),
      rate = deleted.querySelector("a").innerText/uploads.querySelector("a").innerText*100;

deleted.append(`(${+rate.toFixed(2)}% of all uploads)`);

{
    const changes = getElem(/^Post Changes/), rate2 = uploads.querySelector("a").innerText/changes.querySelector("a").innerText*100;
    uploads.querySelector("a").insertAdjacentText("afterend", ` (${+rate2.toFixed(2)}% of post changes)`);
}
See recent deleted profile button (+ percentages)

EDIT: Quick patch to use dataset.userName instead of innerText; underscored usernames are rendered as spaces but this will fail a search, so now it'll use the underscores in searches instead of the rendered name.

I channeled a bit of my morning frustrations into making a small little utility for myself so I can review my deletions. Only really worth a dime when gunning for Contributor or just for the sake of looking if you're someone who falls through the modqueue quite a bit and want to try and find patterns. You can also check others' recent deletions with it.

I wrote this into a fork of the profile percentages userscript so the script below will add both profile percentages and a "see recent deleted" button beside your Deleted Uploads number, similarly to how you can see "tag changes report" beside your Uploads number. This does work for Member-level users; it uses two tag searches (user + -duplicate) and two meta searches that don't count towards the tag limit (status:deleted + age:<=2mo).

Show
// ==UserScript==
// @name more-profile-views
// @description Fork of danbooru-deleted-uploads-percentage to add extra views to user profile
// @match https://*.donmai.us/users/*
// @match https://donmai.moe/users/*
// @match https://*.donmai.us/profile
// @match https://donmai.moe/profile
// @grant none
// @version 1.1-views.2
// ==/UserScript==

function getElem(re) {
    return [...document.querySelectorAll("table.user-statistics th")].filter(th => th.innerText.match(re))[0].nextElementSibling;
}

const uploads = getElem(/^Uploads/),
      deleted = getElem(/^Deleted Uploads/),
      rate = deleted.querySelector("a").innerText/uploads.querySelector("a").innerText*100,
      username = document.getElementsByClassName("user")[0].dataset.userName;

deleted.append(`(${+rate.toFixed(2)}% of all uploads)`);

// Adds a button to check your recent deleted (non-duplicate deletions within 2 months)
(function () {
    var recentDeleted = document.createElement("a");
    var recentDeletedText = document.createTextNode("see recent deleted")

    recentDeleted.appendChild(recentDeletedText)
    recentDeleted.href = `/posts?tags=user%3A${username}+status%3Adeleted+age%3A<%3D2mo+-duplicate&z=5`
    recentDeleted.rel = "nofollow"

    deleted.append(recentDeleted)

    recentDeleted.insertAdjacentText("beforebegin", " (")
    recentDeleted.insertAdjacentText("afterend", ")")
})()

{
    const changes = getElem(/^Post Changes/), rate2 = uploads.querySelector("a").innerText/changes.querySelector("a").innerText*100;
    uploads.querySelector("a").insertAdjacentText("afterend", ` (${+rate2.toFixed(2)}% of post changes)`);
}

Updated

Yooyooko's useful tools

  • Show total deletion percentage on /users/* by default
  • Fetch more info: You get a button on /users/* that fetches more information on users like: Uploads per day, Deletion percentage (last 60 days), Flagged, pending, appealed posts, Related tags of user's uploads, Rating percentages of user's uploads
    This performs several .json fetch requests (about 5-7)
  • Use raw files for transparent and animated posts: Uses raw files for posts tagged with transparent_background or animated
    Caution: This could cause major lagging
  • Use raw files for all thumbnails: Same as above, but without tag discrimination
  • Hot Garbage posts: You get a button in the toolbar on /posts that takes you to (age:<2d order:score_asc status:any) posts
  • Artists on Danbooru: You get a button on /artists that shows you the list of artists who have an account on Danbooru
  • Searching feedbacks and bans by account level, user upload count, user post edit rendition count
  • [BUILDERS+ ONLY] Button for applying tagscript to all visible posts
  • Button on /dmails/new that gives you a set of useful pre-written responses containing links to useful help: and howto: pages
  • /users/* favgroups link - Sort favorite groups alphabetically by default
  • Yooyooko's extra features settings tab that lets you enable/disable some of the features (active-id: 100)
Show
// ==UserScript==
// @name         Danbooru - Yooyooko's extra features
// @namespace    http://tampermonkey.net/
// @version      2024-10-13
// @description  Useful functions for user analitics for Danbooru and other stuff
// @author       yooyooko
// @match        https://danbooru.donmai.us*
// @match        https://danbooru.donmai.us/*
// @match        https://aibooru.online*
// @match        https://aibooru.online/*
// @match        https://gaybooru.app*
// @match        https://gaybooru.app/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=donmai.us
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Yooyooko's Extra Features for Danbooru (+ sister sites)
    // - Show deletion percentage on /users/* by default
    // - Fetch more info: You get a button on /users/* that fetches more information on users like: Uploads per day, Deletion percentage (last 60 days), Flagged, pending, appealed posts, Related tags of user's uploads, Rating percentages of user's uploads
    // - Use raw files for transparent and animated posts: Uses raw files for posts tagged with transparent_background or animated
    // - Use raw files for all thumbnails: Same as above, but without tag discrimination
    // - Hot Garbage posts: You get a button in the toolbar on /posts that takes you to {{(age:<2d order:score_asc status:any)}} posts
    // - Artists on Danbooru: You get a button on /artists that shows you the list of artists who have an account on Danbooru
    // - Searching feedbacks and bans by account level, user upload count, user post edit rendition count
    // - [BUILDERS+ ONLY] Button for applying tagscript to all visible posts
    // - Button on /dmails/new that gives you a set of useful pre-written responses containing links to useful help: and howto: pages
    // - /users/* favgroups link - Sort favorite groups alphabetically by default
    // - Yooyooko's extra features settings tab that lets you enable/disable some of the features (active-id: 100)

    //configuration

    if(localStorage.getItem("yooyooko-extrafeatures-settings-hotgarbagepostslink-enable")==null) localStorage.setItem("yooyooko-extrafeatures-settings-hotgarbagepostslink-enable",1)
    if(localStorage.getItem("yooyooko-extrafeatures-settings-artistsondanboorulink-enable")==null) localStorage.setItem("yooyooko-extrafeatures-settings-artistsondanboorulink-enable",1)
    if(localStorage.getItem("yooyooko-extrafeatures-settings-userawfilesfortransparentandanimatedposts-enable")==null) localStorage.setItem("yooyooko-extrafeatures-settings-userawfilesfortransparentandanimatedposts-enable",0)
    if(localStorage.getItem("yooyooko-extrafeatures-settings-userawfilesforallthumbnails-enable")==null) localStorage.setItem("yooyooko-extrafeatures-settings-userawfilesforallthumbnails-enable",0)

    var hot_garbage_posts_link_e=localStorage.getItem("yooyooko-extrafeatures-settings-hotgarbagepostslink-enable")
    var artists_on_danbooru_link_e=localStorage.getItem("yooyooko-extrafeatures-settings-artistsondanboorulink-enable")
    var use_raw_files_for_transparent_and_animated_posts=localStorage.getItem("yooyooko-extrafeatures-settings-userawfilesfortransparentandanimatedposts-enable")
    var use_raw_files_for_all_thumbnails=localStorage.getItem("yooyooko-extrafeatures-settings-userawfilesforallthumbnails-enable")

    var my_html_elem=null

    if(!myFunctionsYYEF) var myFunctionsYYEF = window.myFunctionsYYEF = {};
    myFunctionsYYEF.htmlToNode = function(html) {
        const template = document.createElement('template');
        template.innerHTML = html;
        const nNodes = template.content.childNodes.length;
        if (nNodes !== 1) {
            throw new Error(
                `html parameter must represent a single node; got ${nNodes}. ` +
                'Note that leading or trailing spaces around an element in your ' +
                'HTML, like " <img/> ", get parsed as text nodes neighbouring ' +
                'the element; call .trim() on your input to avoid this.'
            );
        }
        return template.content.firstChild;
    }

    myFunctionsYYEF.applySettings = function() {
        localStorage.setItem("yooyooko-extrafeatures-settings-hotgarbagepostslink-enable", document.querySelector("#yooyooko-ef-selectinput-hotgarbagepostslink-enable").value );
        localStorage.setItem("yooyooko-extrafeatures-settings-artistsondanboorulink-enable", document.querySelector("#yooyooko-ef-selectinput-artistsondanboorulink-enable").value );
        localStorage.setItem("yooyooko-extrafeatures-settings-userawfilesfortransparentandanimatedposts-enable", document.querySelector("#yooyooko-ef-selectinput-userawfilesfortransparentandanimatedposts-enable").value );
        localStorage.setItem("yooyooko-extrafeatures-settings-userawfilesforallthumbnails-enable", document.querySelector("#yooyooko-ef-selectinput-userawfilesforallthumbnails-enable").value );
    }

    // link for browsing hot garbage posts
    if(hot_garbage_posts_link_e==1) if(document.querySelector("#subnav-hot")) document.querySelector("#subnav-hot").outerHTML+=`<li id="subnav-hotgarbage"><a id="subnav-hotgarbage-link" href="/posts?tags=%28age%3A<2d+order%3Ascore_asc+status%3Aany%29&z=5">Hot Garbage</a></li>`;

    // link for browsing artists with a danbooru account
    if(artists_on_danbooru_link_e==1) if(document.querySelector("#subnav-artists")) document.querySelector("#subnav-artists").outerHTML+=`<li id="subnav-hotgarbage"><a id="subnav-hotgarbage-link" href="/artists?commit=Search&search%5Border%5D=post_count&search%5Burl_matches%5D=%2A${document.querySelector("#app-name").innerHTML.toLowerCase()}%2A">Artists on ${document.querySelector('#app-name').innerHTML}</a></li>`;

    // button for applying tagscript to all visible posts
    if(document.querySelector("#mode-box")) {
        var my_add_button=`<input type="submit" onclick='Array.from(document.querySelectorAll(".post-preview")).forEach(item => item.querySelector("img").click())' id="btn-applytagscripttoallvisible" style="margin-top:5px;" value="Apply to all visible" class="button-primary button-sm">`;
        //document.querySelector("#mode-box h2").onclick=function() {document.querySelector("#mode-box").innerHTML+=`${my_add_button}`;};
        document.querySelector("#mode-box h2").onclick=function() {if(document.querySelector("#btn-applytagscripttoallvisible")==null) document.querySelector("#mode-box").appendChild(myFunctionsYYEF.htmlToNode(my_add_button)) }
    }

    // enable transparent backgrounds for thumbnails
    if(use_raw_files_for_transparent_and_animated_posts==1)
    Array.from(document.querySelectorAll("article.post-preview")).filter( item => (item.dataset.tags.split(" ").includes("transparent_background") || item.dataset.tags.split(" ").includes("animated")) ).forEach(item=>{
        console.log(item.dataset.id)
        fetch(`/posts/${item.dataset.id}.json`)
            .then(response => response.json())
            .then(json => {
            console.log(item.querySelector("img"))
            var selected_asset_variant=json.media_asset.variants[json.media_asset.variants.length-1]
            if(selected_asset_variant.url.endsWith(".zip")) {
                selected_asset_variant=json.media_asset.variants[json.media_asset.variants.length-2]
            }
            item.querySelector("img").src=selected_asset_variant.url
            item.querySelector("img").style=`background-image: linear-gradient(45deg, #1d1d1d 25%, transparent 25%), linear-gradient(-45deg, #1d1d1d 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #1d1d1d 75%), linear-gradient(-45deg, transparent 75%, #1d1d1d 75%); background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0px;`
            item.querySelector("source").srcset=selected_asset_variant.url;
            if(selected_asset_variant.url.endsWith(".mp4") || selected_asset_variant.url.endsWith(".webm") ) {
                //item.querySelector("img").src+="#t=0,5"
                item.querySelector("img").outerHTML=item.querySelector("img").outerHTML.replace("<img","<video autoplay muted loop")
            }
            //console.log(json.media_asset.variants)
        })
    })

    if(use_raw_files_for_all_thumbnails==1)
    Array.from(document.querySelectorAll("article.post-preview")).forEach(item=>{
        console.log(item.dataset.id)
        fetch(`/posts/${item.dataset.id}.json`)
            .then(response => response.json())
            .then(json => {
            console.log(item.querySelector("img"))
            var selected_asset_variant=json.media_asset.variants[json.media_asset.variants.length-1]
            if(selected_asset_variant.url.endsWith(".zip")) {
                selected_asset_variant=json.media_asset.variants[json.media_asset.variants.length-2]
            }
            item.querySelector("img").src=selected_asset_variant.url
            item.querySelector("img").style=`background-image: linear-gradient(45deg, #1d1d1d 25%, transparent 25%), linear-gradient(-45deg, #1d1d1d 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #1d1d1d 75%), linear-gradient(-45deg, transparent 75%, #1d1d1d 75%); background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0px;`
            item.querySelector("source").srcset=selected_asset_variant.url;
            if(selected_asset_variant.url.endsWith(".mp4") || selected_asset_variant.url.endsWith(".webm") ) {
                //item.querySelector("img").src+="#t=0,5"
                item.querySelector("img").outerHTML=item.querySelector("img").outerHTML.replace("<img","<video autoplay muted loop")
            }
            //console.log(json.media_asset.variants)
        })
    })

    if(window.location.href.endsWith("/settings")) {
        document.querySelector(".tab-list").innerHTML+=`
              <a class="tab security-yooyooko-ef" x-on:click.prevent="active = 100" x-bind:class="{ 'active-tab': active === 100 }" href="#">Yooyooko's extra features</a>
           `;
        document.querySelector(".tab-panels").innerHTML+=`
<div class="tab-panel advanced-tab active-tab" x-bind:class="{ 'active-tab': active === 100 }">
   <form class="simple_form edit_user" id="edit_user_1211591" autocomplete="off" novalidate="novalidate" action="/users/1211591" accept-charset="UTF-8" method="post">
      <input type="hidden" name="_method" value="patch" autocomplete="off"><input type="hidden" autocomplete="off">

      <div class="input select optional user_new_post_navigation_layout field_with_hint">
         <label class="select optional" for="user_new_post_navigation_layout">Hot Garbage Posts link</label>
         <select class="select optional" id="yooyooko-ef-selectinput-hotgarbagepostslink-enable">
            <option value="1">Yes</option>
            <option value="0">No</option>
         </select>
         <span class="hint">Show a button that takes you to recently uploaded posts (age:<2d) ordered by their score from lowest to highest</span>
      </div>

      <div class="input select optional user_new_post_navigation_layout field_with_hint">
         <label class="select optional" for="user_new_post_navigation_layout">Artists on Danbooru link</label>
         <select class="select optional" id="yooyooko-ef-selectinput-artistsondanboorulink-enable">
            <option value="1">Yes</option>
            <option value="0">No</option>
         </select>
         <span class="hint">Show a button that takes you to the page that shows the list of artists who have a Danbooru account</span>
      </div>

      <div class="input select optional user_new_post_navigation_layout field_with_hint">
         <label class="select optional" for="user_new_post_navigation_layout">Show animated and transparent thumbnails</label>
         <select class="select optional" id="yooyooko-ef-selectinput-userawfilesfortransparentandanimatedposts-enable">
            <option value="1">Yes</option>
            <option value="0">No</option>
         </select>
         <span class="hint">Enabling this will make Danbooru use raw files as thumbnails for posts tagged as transparent or animated. Enabling this could also cause lags, sending too many web requests, excessive cache and RAM memory build-up and high CPU usage.</span>
      </div>

      <div class="input select optional user_new_post_navigation_layout field_with_hint">
         <label class="select optional" for="user_new_post_navigation_layout">Use raw files for all thumbnails</label>
         <select class="select optional" id="yooyooko-ef-selectinput-userawfilesforallthumbnails-enable">
            <option value="1">Yes</option>
            <option value="0">No</option>
         </select>
         <span class="hint">Enabling this will do the same as above, but it will do so for all posts.</span>
      </div>


   </form>
   <input type="submit" class="btn" data-disable-with="Submit" onclick="myFunctionsYYEF.applySettings()">
</div>
           `
        document.querySelector("#yooyooko-ef-selectinput-hotgarbagepostslink-enable").value=localStorage.getItem("yooyooko-extrafeatures-settings-hotgarbagepostslink-enable");
        document.querySelector("#yooyooko-ef-selectinput-artistsondanboorulink-enable").value=localStorage.getItem("yooyooko-extrafeatures-settings-artistsondanboorulink-enable");
        document.querySelector("#yooyooko-ef-selectinput-userawfilesfortransparentandanimatedposts-enable").value=localStorage.getItem("yooyooko-extrafeatures-settings-userawfilesfortransparentandanimatedposts-enable");
        document.querySelector("#yooyooko-ef-selectinput-userawfilesforallthumbnails-enable").value=localStorage.getItem("yooyooko-extrafeatures-settings-userawfilesforallthumbnails-enable");
    }

    if(/https:.*\/(user_feedbacks|bans).*/.test(window.location.href)) {
        document.querySelectorAll("#a-index form div")[document.querySelectorAll("#a-index form div").length-1].outerHTML+=`

        <div class="input string optional search_reason_matches"><label class="string optional" for="search_reason_matches">User upload count</label><input class="string optional" type="text" name="search[user][post_upload_count]" id="search_upload_count"></div>
        <div class="input string optional search_reason_matches"><label class="string optional" for="search_reason_matches">User note update count</label><input class="string optional" type="text" name="search[user][note_update_count]" id="search_note_update_count"></div>
        <div class="input string optional search_reason_matches"><label class="string optional" for="search_reason_matches">User post update count</label><input class="string optional" type="text" name="search[user][post_update_count]" id="search_post_update_count"></div>

            <div class="input select optional search_level"><label class="select optional" for="search_level">Level</label><select class="select optional" name="search[user][level]" id="search_level"><option value="" label=" "></option>
<option value="10">Restricted</option>
<option value="20">Member</option>
<option value="30">Gold</option>
<option value="31">Platinum</option>
<option value="32">Builder</option>
<option value="35">Contributor</option>
<option value="37">Approver</option>
<option value="40">Moderator</option>
<option value="50">Admin</option>
<option value="60">Owner</option></select></div>
        `
        document.querySelector("#search_level").value=(new URLSearchParams(window.location.search)).get('search[user][level]');
        document.querySelector("#search_note_update_count").value=(new URLSearchParams(window.location.search)).get('search[user][note_update_count]');
        document.querySelector("#search_post_update_count").value=(new URLSearchParams(window.location.search)).get('search[user][post_update_count]');
        document.querySelector("#search_upload_count").value=(new URLSearchParams(window.location.search)).get('search[user][post_upload_count]');
    }

    if(/https:\/\/.*\/dmails\/new.*/.test(window.location.href)) {
        var pre_written_responses=[
            {
                 name:"New user uploading and unaware of rules",
                 title:"Please read the upload rules",
                 message:
`Hello, I see that you’re new to uploading.
Please read [[help:upload rules|The Upload Rules]] before uploading more posts.
`
            },
            {
                 name:"User unresponsive to DMails",
                 title:"Unresponsive to DMails",
                 message:
`Hello, you are being unresponsive to my DMails.
Please take the critic and don’t be ignorant about it.
Especially if it’s because of you breaking the rules or me telling you how not to do things.
`
            },
            {
                 name:"User is incorrectly rating posts",
                 title:"Incorrect rating",
                 message:
`Hello, you’re not rating your posts correctly.
Please take a look at asset #22830992 which gives you great examples of post ratings.
Please do also read the wiki page [[howto:rate]] which gives you a more in depth explanation of post ratings.
`
            },
            {
                 name:"User not adding enough tags",
                 title:"Improper tagging",
                 message:
`Hello, you’re not tagging your posts properly.
Please take a look at [[howto:tag]].
`
            },
            {
                 name:"Self-uploader uploading their art regardless of quality",
                 title:"Self-uploading",
                 message:
`Hello, [[self-upload|uploading your art]] on Danbooru is not against the rules, however the quality of the art needs to follow Danbooru's standards like any other post here.

[[Danbooru_(site)|Danbooru]] is not made for artists. [[Danbooru_(site)|Danbooru]]'s primary purpose is archiving high-quality artwork.
If you want to upload your own artwork, please go to [[DeviantART]], [[Pixiv]], [[Twitter]] or any other website that allows artists to upload their works.

Please take a look at the wiki for more information:
* [[help:upload]]
* [[help:upload rules|upload rules]]
`
            },
            {
                 name:"User is a minor",
                 title:"Please leave this site",
                 message:
`Hello, please do not use Danbooru in any way if you’re under 18 years old.
This website contains explicit imagery and is not suited for such people.
Your account will simply be banned.
`
            },
            {
                 name:"Blatant mistagging",
                 title:"Blatant mistagging",
                 message:
`Hello, please do not put blatantly wrong tags where they do not belong.
Please take a look at [[howto:tag]].
`
            },
            {
                 name:"Comment spamming",
                 title:"Comment spamming",
                 message:
`Hello, please do not write unnecessary stuff in the comments.
This includes “lol”, “wow”, anything too simple or anything that doesn’t add much value to Danbooru.
Stuff like this will either be seen as spam or trolling.

Please check [[help:comments]] and [[help:community_rules]] for more information.
`
            },
            {
                 name:"Comment hostility",
                 title:"Comment hostility",
                 message:
`Hello, please do not write hostile and demeaning comments on Danbooru.
Comments like that are very likely to be deleted and attract negative feedback.

Please check [[help:comments]] and [[help:community_rules]] for more information.
`
            },
            {
                 name:"User uploading AI art",
                 title:"You're uploading AI generated imagery",
                 message:
`Hello, please do not upload fully [[ai-generated]] imagery.
It is against the [[help:upload_rules|upload rules]] to upload [[ai-generated]] images to Danbooru.

There is a “sister”-site to Danbooru called [https://aibooru.online/](AIBooru), which was created only for [[ai-generated]] images and artwork, so please upload [[ai-generated|AI]] images there instead.
There are many people who post [[self-upload]]s there, so if you’re an AI creator yourself you can get decent feedback on your generations and improve them.
`
            },
            {
                 name:"User uploading duplicates",
                 title:"You're uploading duplicates",
                 message:
`Hello, please do not upload [[duplicate|duplicates]].
`
            },
            {
                 name:"User uploading real life images",
                 title:"You're uploading real life images",
                 message:
`Hello, please do not upload [[photo_(medium)|real life photographs]] that have nothing to do with anime, manga or video games.
Stuff like that is blatantly [[off-topic]], therefore against the [[help:upload rules|upload rules]].
`
            },
            {
                 name:"User uploading off-topic content",
                 title:"You're uploading off-topic content",
                 message:
`Hello, you’re uploading [[off-topic]] content which is against the [[help:upload rules|upload rules]].

[u][b]Please check the following wiki pages[/b][/u]:
* [u][b][[help:upload]][/b][/u]
* [[help:upload rules|upload rules]]
* [[off-topic]]
`
            },
            {
                 name:"User uploading low quality content",
                 title:"You're uploading low quality content",
                 message:
`Hello, please do not upload low quality content to [[danbooru_(site)|Danbooru]].

Please check the [[help:upload]] wiki page.
`
            },
            {
                 name:"User uploading blatantly low quality content",
                 title:"You're uploading blatantly low quality content",
                 message:
`Hello, please do not upload blatantly low quality content to Danbooru.

Please check the [[help:upload]] wiki page.
If you do not wish to contribute to this site, please log off now and do not cause further trouble.
`
            },
            {
                 name:"User writing bad wikis",
                 title:"You're writing bad wikis",
                 message:
`Hello, you are not making or editing wiki pages properly.
Wiki pages are supposed to be objectively provided information about a tag.

Please check [[howto:wiki]] for more information.
`
            },
            {
                 name:"User continuously trolls and behaves bad",
                 title:"You're continuously trolling and behaving badly",
                 message:
`Hello, it seems that you are repeatedly breaking the rules after the members, mods and admins told you many times not to break them.
You should leave the site for a bit and reflect upon yourself.
Trolling and behaving like this is unhealthy.
Come back once you actually want to contribute to the site,
unless your account has already been permanently banned.
`
            },
        ]

        document.querySelector("div.dmail_title").innerHTML+=`<br><input value="Quick response" id='temp-to-remove' onclick='myFunctionsYYEF.showResponseButtons()' type="button" style='margin-top:5px;'>`
        myFunctionsYYEF.showResponseButtons = function() {
            document.querySelector("#temp-to-remove").remove();
            pre_written_responses.forEach(response => {
                var my_html_elem_tmp = myFunctionsYYEF.htmlToNode(`
               <input value="${response.name}" onclick='document.querySelector(".dmail_title input").value=\`${response.title.replaceAll("'","’")}\`; document.querySelector("textarea.dtext").value=\`${response.message.replaceAll("'","’")}\`' type="button" style='margin-top:5px;'>
             `.trim());
                document.querySelector("div.dmail_title").appendChild(my_html_elem_tmp)
                my_html_elem_tmp = myFunctionsYYEF.htmlToNode('<br>');
                document.querySelector("div.dmail_title").appendChild(my_html_elem_tmp);
            })
        }
    }

    // show total delete ratio on profile page
    if(/https:\/\/.*\/profile$/.test(window.location.href) || /https:\/\/.*\/users\/\d+$/.test(window.location.href)) {
        var danbooru_username=document.querySelector("#c-users a.user").innerHTML.replaceAll(' ','_')
        var deleted_upload_count=parseInt(Array.from(document.querySelectorAll('th')).find(item => item.innerHTML == 'Deleted Uploads').parentElement.querySelector("td a").innerHTML);
        var   total_upload_count=parseInt(Array.from(document.querySelectorAll('th')).find(item => item.innerHTML == 'Uploads').parentElement.querySelector("td a").innerHTML)
        var deletion_ratio=0;
        if (!(deleted_upload_count==0 || total_upload_count==0)) deletion_ratio=Math.round((deleted_upload_count / total_upload_count)*10000)/100;
        Array.from(document.querySelectorAll('th')).find(item => item.innerHTML == 'Deleted Uploads').parentElement.outerHTML+=
            `
            <tr>
            <th>Deletion percentage</th>
            <td>
            ${deletion_ratio}%
            </td>
            </tr>
            `;

        // fetch more info about a user (with a button)
        document.querySelector("#a-show h1 a").outerHTML+=`&nbsp<a class="dtext-link" style="font-size:16px; font-style: italic; cursor:pointer" onclick="myFunctionsYYEF.showMoreUserInfo();">Fetch more info</a>`

        myFunctionsYYEF.showMoreUserInfo = function() {
            document.querySelector("#a-show h1 a.dtext-link").remove()

            // posts per day statistic
            var dateEnd = new Date(new Date().setDate(new Date().getDate() - 2));
            var dateStart = new Date(new Date().setDate(new Date().getDate() - 32));
            var days_counted=0;
            var posts_per_day=0;
            console.log(`${dateStart.toISOString().slice(0, 10)}`);
            fetch(`/reports/posts.json?commit=Search&id=posts&search%5Bfrom%5D=${dateStart.toISOString().slice(0, 10)}&search%5Bperiod%5D=day&search%5Btags%5D=user%3A${danbooru_username}&search%5Bto%5D=${dateEnd.toISOString().slice(0, 10)}`)
                .then(response => response.json())
                .then(json => {
                json.forEach(item => {
                    posts_per_day+=item.posts;
                })
                posts_per_day=Math.round((posts_per_day/json.length)*100)/100
                console.log(posts_per_day)
                Array.from(document.querySelectorAll('th')).find(item => item.innerHTML == 'Uploads').parentElement.outerHTML+=
                    `
            <tr>
            <th>Uploads per day<br>(last 30 days)</th>
            <td>
            <a href="/reports/posts?commit=Search&id=posts&search%5Bperiod%5D=day&search%5Btags%5D=user%3A${danbooru_username}">${posts_per_day}</a>
            </td>
            </tr>
            `;

            })

            // deletion ratio from last 60days
            fetch(`/counts/posts.json?tags=user:${danbooru_username} age:<=60d`)
                .then(response => response.json())
                .then(json => {

                fetch(`/counts/posts.json?tags=user:${danbooru_username} age:<=60d status:deleted`)
                    .then(response => response.json())
                    .then(json2 => {
                    var deletion_ratio_60days=0;
                    if(json.counts.posts!=0)
                        deletion_ratio_60days=Math.round((json2.counts.posts/json.counts.posts)*10000) / 100
                    var total_upload_count_60days=json.counts.posts;
                    Array.from(document.querySelectorAll('th')).find(item => item.innerHTML == 'Deletion percentage').parentElement.outerHTML+=
                        `
            <tr>
            <th>Deletion percentage <br>(last 60 days)</th>
            <td>
            <a href="/posts?tags=user:${danbooru_username} status:deleted age:<=60d">${deletion_ratio_60days}% (${json2.counts.posts})</a> of <a href="/posts?tags=user:${danbooru_username} age:<=60d">${total_upload_count_60days} posts</a>
            </td>
            </tr>
            `;
                })

            })

            // show flagged post count
            var my_html_elem_flgps=Array.from(document.querySelectorAll('th')).find(item => item.innerHTML == 'Deletion percentage');
            if(my_html_elem_flgps===null) my_html_elem_flgps=Array.from(document.querySelectorAll('th')).find(item => item.innerHTML == 'Deleted Uploads')
            my_html_elem_flgps=my_html_elem_flgps.parentElement;

            fetch(`/counts/posts.json?tags=user%3A${danbooru_username}+status%3Aflagged`)
                .then(response => response.json())
                .then(json => {
                my_html_elem_flgps.outerHTML+=
                    `
            <tr>
            <th>Flagged Posts</th>
            <td>
            <a href="/posts?tags=status%3Aflagged+user%3A${danbooru_username}">${json.counts.posts}</a>
            </td>
            </tr>
            `;

                fetch(`/counts/posts.json?tags=user%3A${danbooru_username}+status%3Apending`)
                    .then(response => response.json())
                    .then(json => {
                    my_html_elem_flgps=Array.from(document.querySelectorAll('th')).find(item => item.innerHTML == 'Flagged Posts');
                    if(my_html_elem_flgps===null) my_html_elem_flgps=Array.from(document.querySelectorAll('th')).find(item => item.innerHTML == 'Deleted Uploads')
                    my_html_elem_flgps=my_html_elem_flgps.parentElement;
                    my_html_elem_flgps.outerHTML+=
                        `
            <tr>
            <th>Pending Posts</th>
            <td>
            <a href="/posts?tags=status%3Apending+user%3A${danbooru_username}">${json.counts.posts}</a>
            </td>
            </tr>
            `;

                    fetch(`/counts/posts.json?tags=is%3Aappealed+appealer%3A${danbooru_username}`)
                        .then(response => response.json())
                        .then(json => {
                        my_html_elem_flgps=Array.from(document.querySelectorAll('th')).find(item => item.innerHTML == 'Pending Posts');
                        if(my_html_elem_flgps===null) my_html_elem_flgps=Array.from(document.querySelectorAll('th')).find(item => item.innerHTML == 'Deleted Uploads')
                        my_html_elem_flgps=my_html_elem_flgps.parentElement;
                        my_html_elem_flgps.outerHTML+=
                            `
            <tr>
            <th>Appealed Posts</th>
            <td>
            <a href="/posts?tags=is%3Aappealed+appealer%3A${danbooru_username}">${json.counts.posts}</a>
            </td>
            </tr>
            `;


                    })


                })

            })

            // redirect saved search links to post search query links
            fetch('/saved_searches.json')
                .then(response => response.json())
                .then(json => {
                json.forEach(item => {
                    var my_html_elem_temp1=document.querySelector(`a[href='/posts?tags=search%3A${encodeURIComponent(item.labels[0]).replaceAll("(","%28").replaceAll(")","%29")}']`)
                    console.log(my_html_elem_temp1)
                    my_html_elem_temp1.href=`/posts?tags=${item.query}`
                })
            })

            // rating percentages
            var html_elem_tmp=Array.from(document.querySelectorAll('th')).find(item => item.innerHTML == 'Favorites').parentElement
            if(html_elem_tmp) {
                fetch('/counts/posts.json?tags=rating:g+user:'+danbooru_username)
                    .then(response => response.json())
                    .then(counts_general => {

                    fetch('/counts/posts.json?tags=rating:s+user:'+danbooru_username)
                        .then(response => response.json())
                        .then(counts_sensitive => {

                        fetch('/counts/posts.json?tags=rating:q+user:'+danbooru_username)
                            .then(response => response.json())
                            .then(counts_questionable => {

                            fetch('/counts/posts.json?tags=rating:e+user:'+danbooru_username)
                                .then(response => response.json())
                                .then(counts_explicit => {
                                var pperc_g=0, pperc_s=0, pperc_q=0, pperc_e=0
                                if(total_upload_count>0) {
                                    pperc_g=Math.round((counts_general.counts.posts / total_upload_count) * 10000)/100;
                                    pperc_s=Math.round((counts_sensitive.counts.posts / total_upload_count) * 10000)/100;
                                    pperc_q=Math.round((counts_questionable.counts.posts / total_upload_count) * 10000)/100;
                                    pperc_e=Math.round((counts_explicit.counts.posts / total_upload_count) * 10000)/100;
                                }
                                html_elem_tmp.outerHTML+=
                                    `<tr>
            <th>Rating percentages</th>
            <td>
               <span><a href='/posts?tags=rating%3Ag+user%3A${danbooru_username}'>General (${pperc_g}%)</a>,
               <a href='/posts?tags=rating%3As+user%3A${danbooru_username}'>Sensitive (${pperc_s}%)</a>,
               <a href='/posts?tags=rating%3Aq+user%3A${danbooru_username}'>Questionable (${pperc_q}%)</a>,
               <a href='/posts?tags=rating%3Ae+user%3A${danbooru_username}'>Explicit (${pperc_e}%)</a> </span>
            </td>
            </tr>`
                            })

                        })

                    })

                })
            }

            // related tags
            // danbooru_username=document.querySelector("#c-users a.user").innerHTML.replaceAll(' ','_')

            if(total_upload_count>0)
                fetch('/related_tag.json?commit=Search&search%5Bcategory%5D=General&search%5Border%5D=Frequency&search%5Bquery%5D=user%3A'+danbooru_username)
                    .then(response => response.json())
                    .then(reltags_general => {

                    fetch('/related_tag.json?commit=Search&search%5Bcategory%5D=Copyright&search%5Border%5D=Frequency&search%5Bquery%5D=user%3A'+danbooru_username)
                        .then(response => response.json())
                        .then(reltags_copyrights => {

                        fetch('/related_tag.json?commit=Search&search%5Bcategory%5D=Character&search%5Border%5D=Frequency&search%5Bquery%5D=user%3A'+danbooru_username)
                            .then(response => response.json())
                            .then(reltags_characters => {

                            fetch('/related_tag.json?commit=Search&search%5Bcategory%5D=Artist&search%5Border%5D=Frequency&search%5Bquery%5D=user%3A'+danbooru_username)
                                .then(response => response.json())
                                .then(reltags_artists => {

                                fetch('/related_tag.json?commit=Search&search%5Bcategory%5D=Meta&search%5Border%5D=Frequency&search%5Bquery%5D=user%3A'+danbooru_username)
                                    .then(response => response.json())
                                    .then(reltags_meta => {
                                    console.log(reltags_meta)

                                    var html_related_tags_str="<span>"
                                    var excluded_general_tags="shirt teeth 2boys 3boys long_sleeves closed_mouth open_mouth pants"
                                    var amount_of_tags=20

                                    excluded_general_tags.split(" ").forEach(taggy =>{
                                        reltags_general.related_tags.splice( reltags_general.related_tags.indexOf(reltags_general.related_tags.find((item) => item.tag.name==taggy)),1 )
                                    })

                                    reltags_general.related_tags.slice(0,amount_of_tags*2).forEach(item => {
                                        html_related_tags_str+=`<a class="tag-type-${item.tag.category}" href="/posts?tags=${item.tag.name} user:${danbooru_username}">${item.tag.name}</a> (${Math.round(item.frequency*10000)/100}%), `
                                    })
                                    html_related_tags_str+="<br>"

                                    reltags_artists.related_tags.slice(0,amount_of_tags).forEach(item => {
                                        html_related_tags_str+=`<a class="tag-type-${item.tag.category}" href="/posts?tags=${item.tag.name} user:${danbooru_username}">${item.tag.name}</a> (${Math.round(item.frequency*10000)/100}%), `
                                    })
                                    html_related_tags_str+="<br>"

                                    reltags_copyrights.related_tags.slice(0,amount_of_tags).forEach(item => {
                                        html_related_tags_str+=`<a class="tag-type-${item.tag.category}" href="/posts?tags=${item.tag.name} user:${danbooru_username}">${item.tag.name}</a> (${Math.round(item.frequency*10000)/100}%), `
                                    })
                                    html_related_tags_str+="<br>"

                                    reltags_characters.related_tags.slice(0,amount_of_tags).forEach(item => {
                                        html_related_tags_str+=`<a class="tag-type-${item.tag.category}" href="/posts?tags=${item.tag.name} user:${danbooru_username}">${item.tag.name}</a> (${Math.round(item.frequency*10000)/100}%), `
                                    })
                                    html_related_tags_str+="<br>"

                                    reltags_meta.related_tags.slice(0,amount_of_tags).forEach(item => {
                                        html_related_tags_str+=`<a class="tag-type-${item.tag.category}" href="/posts?tags=${item.tag.name} user:${danbooru_username}">${item.tag.name}</a> (${Math.round(item.frequency*10000)/100}%), `
                                    })
                                    html_related_tags_str+="<br>"

                                    html_related_tags_str+="</span>"

                                    console.log(html_related_tags_str)
                                    var tb_fav_row=Array.from(document.querySelectorAll('th')).find(item => item.innerHTML == 'Favorites').parentElement
                                    tb_fav_row.outerHTML=
                                        `
            <tr>
            <th>Related tags</th>
            <td>
            ${html_related_tags_str}
            </td>
            </tr>
            `+tb_fav_row.outerHTML;

                                })



                            })

                        })

                    })

                })

        };

    

    // favorite groups sort alphabetically by default
        var myelem=Array.from(document.querySelectorAll('th')).find(item => item.innerHTML == 'Favorite Groups').parentElement.querySelector("td a")
        myelem.href+="&limit=200&search%5Border%5D=name";

        if (Danbooru.PostTooltip.instance) Danbooru.PostTooltip.instance[0].destroy();
        Danbooru.PostTooltip.SHOW_DELAY = 0;
        Danbooru.PostTooltip.initialize();

    }
})();

Updated

Warning notice for artists who post rule-breaking content

This userscript shows a window dialog box that warns you of artists or accounts who post rule-breaking content.
You get a warning message box:

  • If posts under an artist tag have a high deletion deletion rate, which could mean the artist's style or quality probably isn't good for Danbooru
  • If an artist's wiki page mentions the tag AI-generated or AI-assisted which it does if an artist makes such content
  • If an image asset has AI-generated metadata
  • If the artist is banned, not rule-breaking but a somewhat useful notice either way, it may be removed in future versions
  • If an image asset is a pixel-perfect duplicate
  • If an image asset is a photo. Least useful out of all these. It detects this by checking ai tags. It may be removed in future versions.
  • If the account is a known third-party repost account. This one is not reliable, it basically checks if the artist tag name string is in the pre-defined array of the userscript.

Out of all these, the AI-generated and artist high-deletion ratio detections are the most useful.

Show
// ==UserScript==
// @name         Danbooru - Auto-detect rule-breaking content
// @namespace    http://tampermonkey.net/
// @version      0.65
// @description  try to take over the world!
// @author       yooyooko
// @match        https://danbooru.donmai.us/uploads/*/assets/*
// @match        https://danbooru.donmai.us/uploads/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=donmai.us
// @grant        none
// ==/UserScript==
// Created at    2024-09-13

// Additional credits
// I_Copy_Jokes - Message phrasing and wording
// bipface - Dialog message box creation code

(function() {
    'use strict';
    // Configurations
    var play_error_sound = 1;


    // Vars
    var xp_error_audio = new Audio('https://archive.org/download/windowsxpstartup_201910/Windows%20XP%20Error.mp3');
    var third_party_repost_accounts = [
        "https://twitter.com/NucarnivalMemes",
        "https://twitter.com/GayYaoiPorn",
        "https://twitter.com/yaoibaragallery",
        "https://twitter.com/JujutsuTwts"
    ];

    var myFunctions = window.myFunctions = {};
    myFunctions.generate_dtextmessage = function(forwhat,artist_name="",par1="",par2="") {
         if(forwhat=="ai-generated artist wiki page detected") return `<span>This artist is an ai-prompter, according to <a class="tag-type-1" href="/wiki_pages/${artist_name}">their wiki page (${artist_name})</a>. <br><br><b> <a class="dtext-link dtext-wiki-link tag-type-5" href="/wiki_pages/ai-generated">AI-generated</a> works are against <a class="dtext-link dtext-wiki-link" href="/wiki_pages/help%3Aupload_rules">the upload rules</a> of Danbooru.</b><br><br> Upload with caution.</span>`
         if(forwhat=="ai-assisting artist wiki page detected") return `<span>This artist creates artwork assisted by AI, according to <a class="tag-type-1" href="/wiki_pages/${artist_name}">their wiki page (${artist_name})</a>. <br><br><b> <a class="dtext-link dtext-wiki-link tag-type-5" href="/wiki_pages/ai-assisted">AI-assisted</a> works are not against <a class="dtext-link dtext-wiki-link" href="/wiki_pages/help%3Aupload_rules">the upload rules</a> of Danbooru, but they are still likely to be deleted.</b><br><br> Carefully check if this image asset could also have been fully <a class="dtext-link dtext-wiki-link tag-type-5" href="/wiki_pages/ai-generated">AI-generated</a>.<br><br> Upload with caution.</span>`
         if(forwhat=="ai-generated art metadata detected") return `<span><a class="dtext-link dtext-wiki-link tag-type-5" href="/wiki_pages/ai-generated">AI-generated</a> art metadata has been detected in this image asset. <br><br><b> <a class="dtext-link dtext-wiki-link tag-type-5" href="/wiki_pages/ai-generated">AI-generated</a> works are against <a class="dtext-link dtext-wiki-link" href="/wiki_pages/help%3Aupload_rules">the upload rules</a> of Danbooru.</b></span>`
         if(forwhat=="banned artist detected") return `<span><b><a class="tag-type-1" href="/wiki_pages/${artist_name}">${artist_name}</a> is a <a class="break-words tag-type-1" data-tag-name="banned_artist" href="/posts?tags=banned_artist">banned artist</a></b>. <br><br> Upload with caution.</span>`
         if(forwhat=="artist with high delete ratio") return `<span><a class="tag-type-1" href="/wiki_pages/${artist_name}">The artist (${artist_name})</a> has a high ratio of deleted posts <b>(${par1}% of ${par2} uploads)</b>.<br><br>Upload their works with caution.</span>`
         if(forwhat=="pixel-perfect duplicate") return `<span>This image asset is a <a class="dtext-link dtext-wiki-link tag-type-5" href="/wiki_pages/pixel-perfect_duplicate">pixel-perfect duplicate</a> of an existing post. <br><br>Upload with caution.</span>`
         if(forwhat=="possibly a photo") return `<span>This image is possibly a <a class="dtext-link dtext-wiki-link tag-type-5" href="/wiki_pages/photo_%28medium%29">photograph</a>.<br><a class="dtext-link dtext-wiki-link tag-type-5" href="/wiki_pages/off-topic">Off-topic</a> <a class="dtext-link dtext-wiki-link tag-type-5" href="/wiki_pages/photo_%28medium%29">photographs</a> are against the upload rules.<br><br><p>This image could also just be a <a class="dtext-link dtext-wiki-link tag-type-0" href="/wiki_pages/photorealistic">photorealistic</a> artwork or a <a class="dtext-link dtext-wiki-link tag-type-5" href="/wiki_pages/cosplay_photo">cosplay photo</a>, which are allowed.</p><p>Upload with caution.</p></span>`
         if(forwhat=="repost or 3rd party edit account") return `<span>The account (<a href='${par1}'>${artist_name}</a>) you're trying to upload from is possibly a <a class="dtext-link dtext-wiki-link tag-type-5" href="/wiki_pages/third-party_source">third-party repost</a> account.<br><br> <a class="dtext-link dtext-wiki-link tag-type-5" href="/wiki_pages/third-party_source">Third-party source</a> posts are not against <a class="dtext-link dtext-wiki-link" href="/wiki_pages/help%3Aupload_rules">the upload rules</a>, but they are very likely to be deleted if a first party source of that post already exists.<br><br> <b><a class="dtext-link dtext-wiki-link tag-type-5" href="/wiki_pages/third-party_edit">Third-party edits</a> are against <a class="dtext-link dtext-wiki-link" href="/wiki_pages/help%3Aupload_rules">the upload rules</a> of Danbooru.</b> <br><br>Upload with caution.</span>`;

    }

    myFunctions.intrusive_htmlmessage=function(spancontent) {
        document.querySelector(".media-asset-container").innerHTML+=`
            <div class="ai-preventer" style="width: 100%; height: 100%; background: black; position: absolute; z-index: 10; top: 0; display: flex; align-items: center; font-size: 19px; line-height: 30px; background: rgba(0, 0, 0, 0.7); padding: 25px; backdrop-filter:blur(5px); flex-direction: column;">
                ${spancontent}
                <br>
                <button name="button" type="submit" id="ai-preventer-close" onClick="document.querySelector('.ai-preventer').style.display='none'" class="ui-button ui-widget ui-corner-all sub">Ok</button>
            </div>`;
    }

    myFunctions.popup_htmlmessage=function(spancontent) {
        document.body.insertAdjacentHTML('beforeend', `<div id="uploadwarn"><form> ${spancontent} </form></div>`);
        document.getElementById('uploadwarn').firstElementChild.requestSubmit = function() {console.log('(submitting)');};
        Danbooru.Utility.dialog('⚠️ Warning', '#uploadwarn');
        document.querySelector(".ui-widget-overlay").remove()
        document.querySelector(".ui-dialog").style.position="fixed";
        document.querySelector(".ui-dialog-buttonpane").style.opacity="0";

        if (play_error_sound) xp_error_audio.play();

        //document.querySelector(".ui-dialog").style.top="0";

        setTimeout(() => {
            document.querySelector(".ui-dialog .ui-dialog-buttonpane button").remove();
            document.querySelector(".ui-dialog .ui-dialog-buttonpane button").innerHTML="Ok";
            document.querySelector(".ui-dialog .ui-dialog-buttonpane button").onclick=function() {
                setTimeout(() => {
                    document.querySelector("#uploadwarn").remove()
                }, 10);
            }
            document.querySelector(".ui-dialog .ui-dialog-titlebar-close").onclick=function() {
                setTimeout(() => {
                    document.querySelector("#uploadwarn").remove()
                }, 10);
            }
            document.querySelector(".ui-dialog-buttonpane").style.opacity="";
        }, 800);

    }

    myFunctions.generate_htmlmessage= function(spancontent) {
        myFunctions.popup_htmlmessage(spancontent)
        //myFunctions.intrusive_htmlmessage(spancontent);
    }

    var artist_name=document.querySelector(".tag-type-1");
    if(artist_name===null) artist_name="unknown artist"
    else artist_name=artist_name.innerHTML;
    console.log('https://danbooru.donmai.us/wiki_pages/${artist_name}.json')
    if (document.querySelector(".upload-ai-warning") === null) {
        fetch(`https://danbooru.donmai.us/wiki_pages/${artist_name}.json`)
            .then(response => response.json())
            .then(json => {
            if(json.body.toLowerCase().includes('ai-generated')) {
                //console.log("yes")
                myFunctions.generate_htmlmessage(myFunctions.generate_dtextmessage("ai-generated artist wiki page detected",artist_name))
            }
            if(json.body.toLowerCase().includes('ai-assisted')) {
                //console.log("yes")
                myFunctions.generate_htmlmessage(myFunctions.generate_dtextmessage("ai-assisting artist wiki page detected",artist_name))
            }
        })
    }
    else {
        myFunctions.generate_htmlmessage(myFunctions.generate_dtextmessage("ai-generated art metadata detected"))
    }

    if (document.querySelector(".upload-pixel-perfect-duplicate-warning") != null) {
        myFunctions.generate_htmlmessage(myFunctions.generate_dtextmessage("pixel-perfect duplicate",artist_name));
        document.querySelector(".similar-tab").click();
    }

    if(document.querySelector('.ai-preventer') === null) {
        fetch(`https://danbooru.donmai.us/counts/posts.json?tags=${artist_name}`)
            .then(response => response.json())
            .then(json1 => {
            console.log(json1)
            fetch(`https://danbooru.donmai.us/counts/posts.json?tags=${artist_name}+status%3Adeleted`)
                .then(response => response.json())
                .then(json2 => {
                    var del_perc=Math.round((json2.counts.posts / json1.counts.posts) * 10000) / 100;
                    var upl_cnt =json1.counts.posts;
                    if(upl_cnt > 15 && del_perc>45) {
                          myFunctions.generate_htmlmessage(myFunctions.generate_dtextmessage("artist with high delete ratio",artist_name,del_perc,upl_cnt))
                    }
            } )
        } )
        setTimeout(() => {
            if(document.querySelector("a[data-tag-name='ai-generated']") != null && document.querySelector('.ai-preventer') === null) {
                myFunctions.generate_htmlmessage(myFunctions.generate_dtextmessage("ai-generated artist wiki page detected",artist_name))
            }
            if(document.querySelector("a[data-tag-name='banned_artist']") != null && document.querySelector('.ai-preventer') === null) {
                myFunctions.generate_htmlmessage(myFunctions.generate_dtextmessage("banned artist detected",artist_name))
            }
            if(document.querySelector(".source-data-content tr a") != null && document.querySelector('.ai-preventer') === null) {
                var tmp_username=document.querySelector(".source-data-content tr a").innerHTML
                var tmp_href=document.querySelector(".source-data-content tr a").href;
                if(third_party_repost_accounts.includes(tmp_href)) {
                    myFunctions.generate_htmlmessage(myFunctions.generate_dtextmessage("repost or 3rd party edit account",tmp_username,tmp_href))
                }
            }
            if(document.querySelector("a.tag-type-5[href='/posts?tags=photo_%28medium%29']") != null &&
               document.querySelector('.ai-preventer') === null &&
              parseInt(document.querySelector("a.tag-type-5[href='/posts?tags=photo_%28medium%29']").parentElement.querySelector(".text-muted").innerHTML.replace("%","")) > 75) {
                myFunctions.generate_htmlmessage(myFunctions.generate_dtextmessage("possibly a photo"))
            }
        }, 1000);
    }
})();

Would there happen to be a userscript that can block pixel-perfect duplicates? I know by design they're not safe to block but I wonder if it's possible to do something on the user-side to forcefully block them. It would especially be nice for res users who face deletions over it. Among all my deletions, duplicates are by far the most annoying.

Shorekeeper said:

Would there happen to be a userscript that can block pixel-perfect duplicates? I know by design they're not safe to block but I wonder if it's possible to do something on the user-side to forcefully block them. It would especially be nice for res users who face deletions over it. Among all my deletions, duplicates are by far the most annoying.

I guess something like this would work unless you actively try to circumvent it:

Show
// ==UserScript==
// @name        Block PPD posting
// @namespace   Violentmonkey Scripts
// @match       https://danbooru.donmai.us/uploads/*
// @grant       none
// @version     1.0
// @author      user #480070
// @description 10/15/2024, 10:32:32 AM
// ==/UserScript==
if ($(".upload-pixel-perfect-duplicate-warning").length > 0) {
    $(".upload-form input[type='submit']").replaceWith($("<span></span>", {
        text: "Pixel-Perfect duplicate",
        style: "color: red; padding: 0.2em;",
    }));
    $("#form").attr("action", "");
}

Got to try this in action twice during rush hour today and yeah it works. I also tried it on Testbooru, essentially only fails if the post button is hit before the image fully renders.

Updated

Show position and dimensions on note box changes

This userscript can show the position and dimensions of the note box when dragging and keyboard events are triggered, allowing you to layout it accurately.

Show
// ==UserScript==
// @name        Danbooru Note Box Change Helper
// @match       *://*.donmai.us/posts/*
// @grant       none
// @version     0.2
// @author      Sibyl
// @description Show position and dimensions on note box changes.
// ==/UserScript==

const hook = methodName => {
  unsafeWindow.Danbooru.Note.Box.prototype["hooked" + methodName] = unsafeWindow.Danbooru.Note.Box.prototype[methodName];
  unsafeWindow.Danbooru.Note.Box.prototype[methodName] = function () {
    this["hooked" + methodName](...arguments);
    const { id, x, y, w, h } = this.note;
    const prefix = id
      ? `<a href="/notes/${id}" target="_blank">Note #${id}</a> <a href="/note_versions?search%5Bnote_id%5D=${id}" target="_blank">»</a>`
      : "Current note";
    unsafeWindow.Danbooru.Utility.notice(`${prefix} changed: <code style="background-color: transparent;">x: ${x}, y: ${y}, w: ${w}, h: ${h}</code></span>`);
  };
};

// `place_note()` shouldn't be directly hooked; otherwise, a notice will be shown every time the page loads.
hook("on_dragstop");
hook("key_nudge");
hook("key_resize");
Enable non-view mode on current page only

Make the edit, tag script, favorite and unfavorite modes effective only on the current page.

Show
// ==UserScript==
// @name        Enable non-view mode on current page only
// @match       *://*.donmai.us/*
// @grant       none
// @version     0.1
// @author      Sibyl
// @description Make the edit, tag script, favorite and unfavorite modes effective only on the current page.
// ==/UserScript==

document.querySelector("#mode-box select")?.addEventListener("change", () => setTimeout(() => localStorage.setItem("mode", "view")));

Updated

Hide images on Pixiv that were already posted on Danbooru

You need the Pixiv image searches and stuff (PISAS) userscript, you can find it on about:userscripts.

Show
// ==UserScript==
// @name         Pixiv - Hide Posted Danbooru posts
// @namespace    http://tampermonkey.net/
// @version      2024-10-24
// @description  try to take over the world!
// @author       1211591
// @match        https://www.pixiv.net/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=pixiv.net
// @grant        GM_addStyle
// @run-at        document-start
// ==/UserScript==

(function() {
    'use strict';

    GM_addStyle(`
    .sc-9y4be5-2:not(.sc-9y4be5-2:has(.sc-rp5asc-5.hHNegy)):has(.pisas-dummydiv a[style^="color:green"]) {
        opacity:0.5;
        filter:brightness(0.7);
        transition:opacity 0.5s, filter 0.3s;
        /*display:none;*/
        /*Uncomment the line above to hide posted images completely*/
    }
    .sc-9y4be5-2:not(.sc-9y4be5-2:has(.sc-rp5asc-5.hHNegy)):has(.pisas-dummydiv a[style^="color:green"]):hover {
        opacity:1;
        filter:brightness(1);
    }
    `);

    // Your code here...
})();
:Danbooru: banned posts helper

This is a userscript written for members below level 37.

Show
// ==UserScript==
// @name        Status:banned Helper
// @match       *://*.donmai.us/posts*
// @grant       none
// @version     0.5
// @author      Sibyl
// @description This post has been removed because of a takedown request.
// ==/UserScript==

(async () => {
  "use strict";

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

  if (/\/posts\/\d+/.test(location.pathname)) {
    const p = document.querySelector("#page > p:last-child");
    if (p && p.innerText === "This page has been removed because of a takedown request.") {
      p.innerText = "Fetching data...";
      try {
        const html = await (
          await fetch(location, {
            headers: { "X-CSRF-Token": Danbooru.Utility.meta("csrf-token") }
          })
        ).text();
        // rails-ujs not broken
        window._rails_loaded = false;
        document.open();
        document.write(html);
        document.close();

        // Although using document.write() performs better than replacing the <html> element, it can still cause some unexpected issues. If your code throws errors after running this script, try adding a short delay before executing your own code.
        /* await (ms => new Promise(resolve => setTimeout(resolve, ms)))(1000); */
        // Write your code here or after the if statement...
      } catch (e) {
        console.error("Error:", e);
      }
    }
  } else if (location.pathname === "/posts") {
    if (document.body.classList.contains("a-index")) {
      const postContainer = document.querySelector("#posts > div.post-gallery > div.posts-container");
      if (!postContainer) return;
      const userPerPage = Danbooru.CurrentUser.data("per-page");
      const postCount = postContainer.children.length;
      if (postCount === userPerPage) return;
      let newUrl = new URL(location);
      let searchText = (newUrl.searchParams.get("tags") || document.getElementById("tags").value).trim();
      if (/\border:random\b/.test(searchText)) return;
      const showDeleted = /\bstatus:(deleted|any)\b/.test(searchText) || Danbooru.CurrentUser.data("show-deleted-posts");
      newUrl.pathname = "/posts.json";

      const iconsHash = document.querySelector("a#close-notice-link use").href.baseVal.split(/-|\./)[1];
      const hideScore = Danbooru.Cookie.get("post_preview_show_votes") == "false";
      const postPreviewSize = Danbooru.Cookie.get("post_preview_size") || "180";

      let thumbnailData = JSON.parse(localStorage.getItem("banned_post_helper"));
      if (!thumbnailData || thumbnailData.id !== CUSTOM_THUMBNAIL) {
        thumbnailData = await (await fetch(`/media_assets/${CUSTOM_THUMBNAIL}.json`)).json();
        if (!thumbnailData || thumbnailData.error) {
          // prettier-ignore
          thumbnailData={variants:[{type:"180x180",url:"https://cdn.donmai.us/180x180/3e/3c/3e3c7baac2a12a0936ba1f62a46a3478.jpg",width:180,height:135,file_ext:"jpg"},{type:"360x360",url:"https://cdn.donmai.us/360x360/3e/3c/3e3c7baac2a12a0936ba1f62a46a3478.jpg",width:360,height:270,file_ext:"jpg"},{type:"720x720",url:"https://cdn.donmai.us/720x720/3e/3c/3e3c7baac2a12a0936ba1f62a46a3478.webp",width:720,height:540,file_ext:"webp"}]}
        }
        localStorage.setItem("banned_post_helper", JSON.stringify(thumbnailData));
      }
      let matchedThumbnailSize;
      switch (postPreviewSize) {
        case "150":
        case "180":
          matchedThumbnailSize = "180x180";
          break;
        case "225":
        case "270":
        case "360":
          matchedThumbnailSize = "360x360";
          break;
        case "720":
          matchedThumbnailSize = "720x720";
        default:
          break;
      }
      let { width, height, url } = thumbnailData.variants.filter(info => {
        return info.type === matchedThumbnailSize;
      })[0];

      let a = document.createElement("a");
      a.id = "check_banned_posts";
      a.href = "#";
      a.title = "Shortcut is c";
      a.setAttribute("data-shortcut", "c");
      a.innerHTML = "<i>Banned</i>";
      document.getElementById("show-posts-link").closest("li").insertAdjacentElement("beforeend", a);
      Danbooru.Shortcuts.initialize_data_shortcuts();
      a.addEventListener("click", function (event) {
        event.preventDefault();
        a.innerHTML = "<i>Checking...</i>";
        fetch(newUrl)
          .then(response => response.json())
          .then(posts => {
            let bannedPostsCount = posts.filter(post => post.is_banned === true).length;
            if (!showDeleted) posts = posts.filter(post => !post.is_deleted);
            let currentPosts = Array.from(postContainer.children);
            const currentPostIds = currentPosts.map(el => {
              return Number(el.getAttribute("data-id"));
            });
            currentPostIds.push(0);
            let idx = 0,
              bannedToShow = 0,
              postsLength = posts.length;
            currentPostIds.forEach((pid, index) => {
              let htmlToInsert = "";
              while (idx < postsLength && posts[idx].id !== pid) {
                if (posts[idx].is_banned) {
                  htmlToInsert += parsingPostData(posts[idx]);
                  bannedToShow++;
                }
                idx++;
              }
              idx++;
              if (htmlToInsert) {
                if (pid === 0) {
                  postContainer.insertAdjacentHTML("afterbegin", htmlToInsert);
                } else currentPosts[index].insertAdjacentHTML("beforebegin", htmlToInsert);
              }
            });
            let msg = "";
            if (bannedPostsCount === 0 && bannedToShow === 0) msg = "No banned posts found.";
            else if (bannedToShow === 0 && bannedPostsCount > bannedToShow) {
              if (bannedPostsCount === 1) msg = "1 banned post found.";
              else msg = `${bannedPostsCount} banned posts found.`;
            } else {
              if (bannedToShow === 1) msg = "Show 1 banned post.";
              else msg = `Show ${bannedToShow} banned posts.`;
              if (bannedPostsCount != bannedToShow) {
                msg += ` ${bannedPostsCount} posts found in total.`;
              }
            }
            Danbooru.Utility.notice(msg);
            $(a)
              .html('<i style="color:var(--success-color)">Finished.</i>')
              .fadeOut("slow", function () {
                $(this).remove();
              });
          })
          .catch(e => {
            console.error("Error:", e);
            a.innerHTML = '<i style="color:var(--error-color)">Failure</i>';
          });
      });
      function parsingPostData({ id, uploader_id, score, rating, tag_string, is_pending, is_flagged, is_deleted, has_children, parent_id }) {
        const dataFlag = is_pending ? "pending" : is_flagged ? "flagged" : is_deleted ? "deleted" : "";
        const isBlacklisted = Danbooru.Blacklist.entries.some(entry => {
          if (entry.disabled) {
            return false;
          }
          let tags = Danbooru.Utility.splitWords(tag_string);
          tags.push("rating:" + rating);
          tags.push("uploaderid:" + uploader_id);
          tags.push("status:" + dataFlag);
          let score_test = entry.min_score === null || score < entry.min_score;
          return (
            Danbooru.Utility.is_subset(tags, entry.require) &&
            score_test &&
            (!entry.optional.length || Danbooru.Utility.intersect(tags, entry.optional).length) &&
            !Danbooru.Utility.intersect(tags, entry.exclude).length
          );
        });

        const classList = ["post-preview", "post-preview-" + postPreviewSize, "post-preview-fit-compact", "blacklisted"];
        !hideScore && classList.push("post-preview-show-votes");
        is_pending && classList.push("post-status-pending");
        is_pending && 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");
        isBlacklisted && classList.push("blacklisted-active");
        const scorePart = hideScore
          ? ""
          : `<div class="post-preview-score text-sm text-center mt-1">
<span class="post-votes inline-flex gap-1" data-id="${id}">
<a class="post-upvote-link inactive-link" data-remote="true" rel="nofollow" data-method="post" href="/posts/${id}/votes?score=1">
<svg class="icon svg-icon upvote-icon" viewBox="0 0 448 512">
<use fill="currentColor" href="/packs/static/icons-${iconsHash}.svg#arrow-alt-up"></use>
</svg></a>
<span class="post-score inline-block text-center whitespace-nowrap align-middle min-w-4">
<a rel="nofollow" href="/post_votes?search%5Bpost_id%5D=${id}&amp;variant=compact">${score}</a></span>
<a class="post-downvote-link inactive-link" data-remote="true" rel="nofollow" data-method="post" href="/posts/${id}/votes?score=-1">
<svg class="icon svg-icon downvote-icon" viewBox="0 0 448 512">
<use fill="currentColor" href="/packs/static/icons-${iconsHash}.svg#arrow-alt-down"></use>
</svg></a></span></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>
${scorePart}
</article>`;
      }
    }
  }
})();

Updated

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
        }

    }

})();

Updated

Nameless_Contributor said:

Simply add this to your custom CSS in advanced settings:

/* Hide recent uploads from user profiles */
.user-uploads {
    display: none;
}

Although, I kinda hope there is a version that only affects my profile page only, since the CSS affects other users as well.

I probably should've specified better that I wanted an "Upload hider" that only affects my profile page and not other users.

Jerrcheron said:

Although, I kinda hope there is a version that only affects my profile page only, since the CSS affects other users as well.

I probably should've specified better that I wanted an "Upload hider" that only affects my profile page and not other users.

/* Hide recent uploads from own user profile */
body:has(#subnav-settings-link) .user-uploads {
    display: none;
}

Jerrcheron said:

I probably should've specified better that I wanted an "Upload hider" that only affects my profile page and not other users.

Something like this perhaps?

Collapse the section. Unhide with a button.
// ==UserScript==
// @name         Collapsible Recent Uploads on Personal Danbooru Profile Page 
// @version      0.1
// @description  Make the user-uploads recent-posts section collapsible on danbooru.donmai.us/profile
// @author       
// @match        https://danbooru.donmai.us/profile
// @grant        none
// ==/UserScript==

(function() {
    'use strict';
    
    // Create a button to toggle the section
    var toggleButton = document.createElement('button');
    toggleButton.textContent = 'Toggle Recent Posts';
    toggleButton.style.display = 'block';
    toggleButton.style.margin = '10px 0';

    // Get the section
    var section = document.querySelector('.user-uploads.recent-posts');

    // Insert the button before the section
    if (section) {
        section.parentNode.insertBefore(toggleButton, section);

        // Set the section to be hidden by default
        section.style.display = 'none';

        // Add an event listener to the button to toggle the section visibility
        toggleButton.addEventListener('click', function() {
            if (section.style.display === 'none') {
                section.style.display = 'block';
            } else {
                section.style.display = 'none';
            }
        });
    }
})();
If you just want it gone.
// ==UserScript==
// @name         Remove Recent Uploads on Personal Danbooru Profile Page 
// @version      0.1
// @description  Hide the user-uploads recent-posts section on danbooru.donmai.us/profile
// @author       
// @match        https://danbooru.donmai.us/profile
// @grant        none
// ==/UserScript==

(function() {
    'use strict';
    var section = document.querySelector('.user-uploads.recent-posts');
    if (section) {
        section.style.display = 'none';
    }
})();

GabrielWB said:

Something like this perhaps?

Collapse the section. Unhide with a button.
// ==UserScript==
// @name         Collapsible Recent Uploads on Personal Danbooru Profile Page
// @version      0.1
// @description  Make the user-uploads recent-posts section collapsible on danbooru.donmai.us/profile
// @author
// @match        https://danbooru.donmai.us/profile
// @grant        none
// ==/UserScript==

(function() {
'use strict';

// Create a button to toggle the section
var toggleButton = document.createElement('button');
toggleButton.textContent = 'Toggle Recent Posts';
toggleButton.style.display = 'block';
toggleButton.style.margin = '10px 0';

// Get the section
var section = document.querySelector('.user-uploads.recent-posts');

// Insert the button before the section
if (section) {
section.parentNode.insertBefore(toggleButton, section);

// Set the section to be hidden by default
section.style.display = 'none';

// Add an event listener to the button to toggle the section visibility
toggleButton.addEventListener('click', function() {
if (section.style.display === 'none') {
section.style.display = 'block';
} else {
section.style.display = 'none';
}
});
}
})();
If you just want it gone.
// ==UserScript==
// @name         Remove Recent Uploads on Personal Danbooru Profile Page
// @version      0.1
// @description  Hide the user-uploads recent-posts section on danbooru.donmai.us/profile
// @author
// @match        https://danbooru.donmai.us/profile
// @grant        none
// ==/UserScript==

(function() {
'use strict';
var section = document.querySelector('.user-uploads.recent-posts');
if (section) {
section.style.display = 'none';
}
})();

It works, thanks :CatJam:

1 6 7 8 9 10