Skip to content

Instantly share code, notes, and snippets.

@srndpty
Created October 21, 2025 08:26
Show Gist options
  • Save srndpty/d7c7277cb3915fc87dfb18ee0754da25 to your computer and use it in GitHub Desktop.
Save srndpty/d7c7277cb3915fc87dfb18ee0754da25 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Misskey Danbooru Checker
// @namespace https://github.com/
// @version 2.0
// @description A unified, cached, and more informative Danbooru checker for Misskey notes.
// @author srndpty
// @match https://misskey.io/*
// @connect danbooru.donmai.us
// @grant GM.xmlHttpRequest
// @grant GM_addStyle
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// --- START: CONFIGURATION ---
// A single selector that matches a note on ANY page (timeline or single view)
const NOTE_SELECTOR = 'article.x5yeR, article.xexC6';
// Selectors for finding elements WITHIN a note
const PERMALINK_SELECTOR = 'a[href^="/notes/"]';
const INJECTION_POINT_SELECTOR = 'footer.xhAPG, article.xexC6 > footer';
const MEDIA_CONTAINER_SELECTOR = 'div.xbIzI';
// --- END: CONFIGURATION ---
// In-memory cache to store results for the session
const checkedNotesCache = new Map();
console.log('Danbooru Checker: Script is now running (v2.0).');
GM_addStyle(`
.danbooru-checker-link {
margin: 0 8px; font-weight: bold; text-decoration: none !important;
padding: 2px 6px; border-radius: 4px; font-size: 0.9em;
display: inline-flex; align-items: center; height: 32px;
}
.danbooru-checker-found { color: #fff !important; background-color: #008000; }
.danbooru-checker-not-found { color: #fff !important; background-color: #d9534f; }
.danbooru-checker-checking { color: #555 !important; background-color: #eee; }
.danbooru-no-media-indicator {
margin-left: 8px; opacity: 0.3; cursor: default;
display: inline-flex; align-items: center; user-select: none;
}
`);
/**
* Builds and injects the final link based on API or cache result.
* @param {object} result - The result object, e.g., {status: 'found', id: 123}.
* @param {string} noteUrl - The URL of the note.
* @param {HTMLElement} injectionElement - The DOM element to append the link to.
*/
function buildLink(result, noteUrl, injectionElement) {
// Clear any previous links (like "Checking...")
injectionElement.querySelectorAll('.danbooru-checker-link').forEach(el => el.remove());
const link = document.createElement('a');
link.className = 'danbooru-checker-link';
link.target = '_blank';
link.rel = 'noopener noreferrer';
if (result.status === 'found') {
link.href = `https://danbooru.donmai.us/posts/${result.id}`;
link.textContent = `Found (#${result.id})`;
link.classList.add('danbooru-checker-found');
} else { // 'not_found'
link.href = `https://danbooru.donmai.us/uploads/new?url=${encodeURIComponent(noteUrl)}`;
link.textContent = 'Upload';
link.classList.add('danbooru-checker-not-found');
}
injectionElement.appendChild(link);
}
/**
* A single, unified function to process any note element.
* @param {HTMLElement} noteElement - The note article element.
*/
function checkNote(noteElement) {
if (noteElement.dataset.danbooruChecked) return;
const injectionPoint = noteElement.querySelector(INJECTION_POINT_SELECTOR);
if (!injectionPoint) { // Can't do anything without a place to inject
noteElement.dataset.danbooruChecked = 'failed-no-injection-point';
return;
}
// Feature: Skip notes without media and provide visual feedback
if (!noteElement.querySelector(MEDIA_CONTAINER_SELECTOR)) {
noteElement.dataset.danbooruChecked = 'no-media';
if (!injectionPoint.querySelector('.danbooru-no-media-indicator')) {
const noMediaIndicator = document.createElement('span');
noMediaIndicator.textContent = 'no media';
noMediaIndicator.title = 'Danbooru Checker: Skipped (no media)';
noMediaIndicator.className = 'danbooru-no-media-indicator';
injectionPoint.appendChild(noMediaIndicator);
}
return;
}
// Determine the note's URL
const permalinkElement = noteElement.querySelector(PERMALINK_SELECTOR);
const noteUrl = permalinkElement ? permalinkElement.href : window.location.href;
noteElement.dataset.danbooruChecked = 'checking';
// Feature: Use cache if available
if (checkedNotesCache.has(noteUrl)) {
buildLink(checkedNotesCache.get(noteUrl), noteUrl, injectionPoint);
return;
}
const tempLink = document.createElement('a');
tempLink.className = 'danbooru-checker-link danbooru-checker-checking';
tempLink.textContent = 'Danbooru...';
injectionPoint.appendChild(tempLink);
GM.xmlHttpRequest({
method: 'GET',
url: `https://danbooru.donmai.us/posts.json?tags=source:${encodeURIComponent(noteUrl)}&limit=1`,
onload: function(response) {
try {
const posts = JSON.parse(response.responseText);
let result;
if (posts && posts.length > 0) {
result = { status: 'found', id: posts[0].id };
} else {
result = { status: 'not_found' };
}
checkedNotesCache.set(noteUrl, result); // Save to cache
buildLink(result, noteUrl, injectionPoint);
} catch (e) {
console.error('Danbooru Checker: Failed to parse response.', e);
}
},
onerror: function(error) {
console.error('Danbooru Checker: Network request failed.', error);
tempLink.remove();
}
});
}
/**
* Initializes the script.
*/
function init() {
// This function will be called by the observer whenever the DOM changes.
const processAllVisibleNotes = () => {
document.querySelectorAll(NOTE_SELECTOR).forEach(checkNote);
};
const observer = new MutationObserver(processAllVisibleNotes);
observer.observe(document.body, {
childList: true,
subtree: true
});
// Run once on initial load
processAllVisibleNotes();
console.log('Danbooru Checker: Unified observer initialized.');
}
if (document.readyState === 'complete') {
init();
} else {
window.addEventListener('load', init);
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment