Skip to content

Instantly share code, notes, and snippets.

@mdashlw
Last active January 28, 2026 21:20
Show Gist options
  • Select an option

  • Save mdashlw/5bf72993f8d0f071d4be1cce1e56cedc to your computer and use it in GitHub Desktop.

Select an option

Save mdashlw/5bf72993f8d0f071d4be1cce1e56cedc to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Booru Dupes Metadata
// @version 2.0.0
// @author mdashlw
// @namespace Booru Scripts
// @match https://*.derpibooru.org/*
// @match https://*.trixiebooru.org/*
// @match https://*.tantabus.ai/*
// @match https://*.furbooru.org/*
// @grant GM_xmlhttpRequest
// @description 3/4/2025
// @updateURL https://gist.github.com/mdashlw/5bf72993f8d0f071d4be1cce1e56cedc/raw/booru-dupes-metadata.user.js
// @downloadURL https://gist.github.com/mdashlw/5bf72993f8d0f071d4be1cce1e56cedc/raw/booru-dupes-metadata.user.js
// ==/UserScript==
if (location.pathname !== "/duplicate_reports") {
return;
}
function xhr(details) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
...details,
timeout: 30_000,
anonymous: true,
onerror: (responseObject) => {
reject("Error!");
console.error("XHR error response object:", responseObject);
},
ontimeout: () => {
reject("Timeout!");
},
onload: (responseObject) => {
if (responseObject.status !== 200) {
reject(
`Error! ${responseObject.status} ${responseObject.statusText}`,
);
console.error("XHR error response object:", responseObject);
return;
}
resolve(responseObject.response);
},
});
});
}
function magick(url) {
return xhr({
method: "GET",
url: `https://duplicatebooru.zipfiled.info/api?url=${encodeURIComponent(
url,
)}`,
responseType: "json",
});
}
async function fetchImages(ids) {
const everythingFilterId = "56027";
const response = await xhr({
method: "GET",
url: `/api/v1/json/search?${new URLSearchParams({
filter_id: everythingFilterId,
q: ids.map((id) => `id:${id}`).join(" OR "),
})}`,
responseType: "json",
});
return response.images;
}
function formatFraction(n, digits) {
return new Intl.NumberFormat("en-US", {
maximumFractionDigits: digits,
}).format(n);
}
function formatBytes(bytes, binary) {
if (!bytes) {
return "";
}
const b = binary ? 1024 : 1000;
const units = binary ? ["Bytes", "KiB", "MiB"] : ["Bytes", "kB", "MB"];
const i = Math.floor(Math.log(bytes) / Math.log(b));
const value = bytes / Math.pow(b, i);
return `${formatFraction(value, 1)} ${units[i]}`;
}
function limitWidthText(text, maxWidth) {
const div = document.createElement("div");
div.textContent = text;
div.title = text;
div.style.maxWidth = maxWidth;
div.style.overflow = "hidden";
div.style.textOverflow = "ellipsis";
div.style.whiteSpace = "nowrap";
return div;
}
for (const diffCell of document.querySelectorAll(
".grid--dupe-report-list__cell.dr__diff",
)) {
const targetCell = diffCell.previousSibling;
const sourceCell = targetCell.previousSibling;
const metadataButton = document.createElement("button");
metadataButton.type = "button";
metadataButton.className = "button";
metadataButton.textContent = "Metadata";
metadataButton.style.fontSize = "1.2em";
metadataButton.style.marginTop = "1em";
metadataButton.style.marginBottom = "0.5em";
metadataButton.addEventListener("click", () => {
metadataButton.remove();
const imageContainers = [
sourceCell.querySelector(".image-container"),
targetCell.querySelector(".image-container"),
];
const inputImageIds = imageContainers.map((c) => Number(c.dataset.imageId));
const inputImageUrls = imageContainers.map((c) =>
JSON.parse(c.dataset.uris).full.replace("/view/", "/download/"),
);
diffCell.appendChild(document.createElement("br"));
Promise.all([
fetchImages(inputImageIds),
...inputImageUrls.map(magick),
]).then(([images, ...infos]) => {
const inputs = inputImageIds.map((id, idx) => ({
image: images.find((i) => i.id === id),
info: infos.find((i) => i.__src === inputImageUrls[idx]),
container: imageContainers[idx],
}));
const view = [
["Hash", ({ info }) => [info.properties.signature]],
["File size", ({ info }) => [formatBytes(info.__size, true)]],
["Format", ({ info }) => [info.format]],
[
"Dimensions",
({ info }) => [`${info.geometry.width}x${info.geometry.height}`],
],
[
"Megapixels",
({ info }) => [formatFraction(info.pixels / 1_000_000, 2)],
],
["Color space", ({ info }) => [info.colorspace]],
["Type", ({ info }) => [info.type]],
["Depth", ({ info }) => [`${info.depth}-bit`]],
["Compression", ({ info }) => [info.compression]],
["Interlacing", ({ info }) => [info.interlace]],
["Orientation", ({ info }) => [info.orientation]],
["JPEG quality", ({ info }) => [info.quality]],
[
"JPEG sampling factors",
({ info }) => [info.properties["jpeg:sampling-factor"]],
],
[
"PNG color type",
({ info }) => [info.properties["png:IHDR.color_type"]],
],
[
"PNG bit depth",
({ info }) => [info.properties["png:IHDR.bit_depth"]],
],
[
"PNG palette colors",
({ info }) => [info.properties["png:PLTE.number_colors"]],
],
[
"Original file size",
({ image }) => [formatBytes(image?.orig_size, true)],
],
[
"Original file name",
({ image }) => [image?.name],
{ maxWidth: "60ch" },
],
[
"Sources",
({ image, container }) =>
image?.source_urls ?? JSON.parse(container.dataset.sourceUrls),
{ maxWidth: "60ch" },
],
];
if (new Set(infos.map((info) => info.properties.signature)).size === 1) {
view.unshift(["Pixel-identical?", () => ["Yes"]]);
}
const table = document.createElement("table");
const tbody = document.createElement("tbody");
table.appendChild(tbody);
for (const [row, fn, options] of view) {
const values = inputs.map(fn);
const max = Math.max.apply(
null,
values.map((v) => v.length),
);
if (max === 0 || values.every((l) => l.every((v) => !v))) {
continue;
}
for (let i = 0; i < max; i++) {
const tr = document.createElement("tr");
const td = document.createElement("td");
td.textContent = row;
tr.appendChild(td);
for (const v of values) {
const td = document.createElement("td");
const value = v[i] || "";
if (options?.maxWidth) {
td.appendChild(limitWidthText(value, options.maxWidth));
} else {
td.textContent = value;
}
tr.appendChild(td);
}
tbody.appendChild(tr);
}
}
diffCell.appendChild(table);
});
});
diffCell.appendChild(metadataButton);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment