aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2026-05-18 03:16:58 -0700
committerPinapelz <yukais@pinapelz.com>2026-05-18 03:16:58 -0700
commit062b7cd85b4c9715ad22dda5f1d89f2e2d5fd699 (patch)
treec354d5f578db801e434cdbf047db7d47b5dd855a
parent2f277121587f944eb059012eb660765ef040097b (diff)
add tiermaker export userscript
-rw-r--r--tiermaker.user.js371
1 files changed, 371 insertions, 0 deletions
diff --git a/tiermaker.user.js b/tiermaker.user.js
new file mode 100644
index 0000000..cabba50
--- /dev/null
+++ b/tiermaker.user.js
@@ -0,0 +1,371 @@
+// ==UserScript==
+// @name Tiermaker Liberator
+// @namespace interactive-tierlist
+// @version 1.0.0
+// @description Export tierlist as JSON with rows + untiered; convert remote image URLs to data: URLs
+// @match *://tiermaker.com/*
+// @match file://*/*
+// @run-at document-idle
+// @grant GM_xmlhttpRequest
+// @connect *
+// ==/UserScript==
+
+(function () {
+ 'use strict';
+
+ const UNTIERED_RE = /untiered|unranked|not\s*tiered|remaining/i;
+ const dataUrlCache = new Map();
+
+ function textOf(el) {
+ return (el?.textContent || '').replace(/\s+/g, ' ').trim();
+ }
+
+ function firstNonEmpty(values) {
+ for (const v of values) {
+ if (typeof v === 'string' && v.trim() !== '') return v.trim();
+ }
+ return '';
+ }
+
+ function normalizeUrl(src) {
+ if (!src) return '';
+ if (src.startsWith('data:')) return src;
+ try {
+ return new URL(src, location.href).href;
+ } catch {
+ return src;
+ }
+ }
+
+ function isLikelyRowName(name) {
+ if (!name) return false;
+ if (name.length > 48) return false;
+ if (/share|download|copy|login|sign in|facebook|twitter|settings/i.test(name)) return false;
+ return true;
+ }
+
+ function uniqueElements(arr) {
+ return [...new Set(arr)];
+ }
+
+ function getRowImageElements(rowEl) {
+ const explicit = Array.from(
+ rowEl.querySelectorAll('.character img, .draggable img, img.character, img.draggable')
+ );
+
+ const imgs = explicit.length ? explicit : Array.from(rowEl.querySelectorAll('img'));
+
+ return uniqueElements(
+ imgs.filter((img) => {
+ const src = img.currentSrc || img.getAttribute('src') || '';
+ if (!src) return false;
+ const w = img.naturalWidth || img.width || 0;
+ const h = img.naturalHeight || img.height || 0;
+ if ((w > 0 && w <= 2) || (h > 0 && h <= 2)) return false;
+
+ return true;
+ })
+ );
+ }
+
+ function extractRowName(rowEl) {
+ const candidates = [];
+ candidates.push(
+ ...rowEl.querySelectorAll(
+ '[class*="label"], [class*="tier"], [class*="rank"], span, h2, h3, h4, p'
+ )
+ );
+
+ for (const el of candidates) {
+ const t = textOf(el);
+ if (isLikelyRowName(t)) return t;
+ }
+
+ return '';
+ }
+
+ function collectRowsFromParent(parentEl) {
+ const rows = [];
+
+ for (const child of Array.from(parentEl.children)) {
+ const imgs = getRowImageElements(child);
+ if (!imgs.length || imgs.length > 400) continue;
+
+ const name = extractRowName(child);
+ if (!isLikelyRowName(name)) continue;
+
+ rows.push({ name, imgEls: imgs, rowEl: child });
+ }
+
+ return rows;
+ }
+
+ function findBestRowSet() {
+ const main = document.querySelector('#main-container') || document.body;
+
+ const seedSelectors = [
+ '#char-tier-container-scroll',
+ '#char-tier-outer-container-scroll',
+ '#char-tier-container',
+ '[id*="char-tier"]',
+ '[id*="tier-container"]',
+ '[class*="tier-container"]',
+ '[class*="tier-list"]',
+ ];
+
+ const seeds = new Set();
+ for (const sel of seedSelectors) {
+ for (const el of document.querySelectorAll(sel)) seeds.add(el);
+ }
+
+ // Fallback seeds
+ for (const el of Array.from(main.querySelectorAll('[id*="tier"], [class*="tier"]')).slice(0, 200)) {
+ seeds.add(el);
+ }
+ seeds.add(main);
+
+ let bestRows = [];
+ let bestParent = null;
+
+ for (const seed of seeds) {
+ const parentCandidates = [seed, ...Array.from(seed.children)];
+ for (const parent of parentCandidates) {
+ const rows = collectRowsFromParent(parent);
+ if (rows.length > bestRows.length) {
+ bestRows = rows;
+ bestParent = parent;
+ }
+ }
+ }
+
+ return { rows: bestRows, parent: bestParent };
+ }
+
+ function findUntieredElements(searchRoot, usedRowEls) {
+ const selectors = [
+ '[id*="untiered"]',
+ '[class*="untiered"]',
+ '[id*="unranked"]',
+ '[class*="unranked"]',
+ ];
+
+ for (const sel of selectors) {
+ for (const el of searchRoot.querySelectorAll(sel)) {
+ if (usedRowEls.has(el)) continue;
+ const imgs = getRowImageElements(el);
+ if (imgs.length) return imgs;
+ }
+ }
+
+ return [];
+ }
+
+ function detectTitle() {
+ const h1 = textOf(document.querySelector('h1'));
+ const og = document.querySelector('meta[property="og:title"]')?.getAttribute('content') || '';
+ let title = firstNonEmpty([h1, og, document.title, 'Untitled Tierlist']);
+ title = title
+ .replace(/\|\s*TierMaker.*$/i, '')
+ .replace(/\s*Tier List Maker.*$/i, '')
+ .trim();
+ return title || 'Untitled Tierlist';
+ }
+
+ function getImageTitle(img) {
+ return firstNonEmpty([
+ img.getAttribute('data-title'),
+ img.getAttribute('title'),
+ img.getAttribute('alt'),
+ img.closest('[data-title]')?.getAttribute('data-title'),
+ img.closest('[title]')?.getAttribute('title'),
+ '',
+ ]);
+ }
+
+ function getImageDescription(img) {
+ return firstNonEmpty([
+ img.getAttribute('data-description'),
+ img.getAttribute('data-desc'),
+ img.closest('[data-description]')?.getAttribute('data-description'),
+ img.closest('[data-desc]')?.getAttribute('data-desc'),
+ '',
+ ]);
+ }
+
+ function blobToDataUrl(blob) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onerror = () => reject(new Error('Failed converting blob to data URL'));
+ reader.onload = () => resolve(String(reader.result));
+ reader.readAsDataURL(blob);
+ });
+ }
+
+ function gmFetchBlob(url) {
+ return new Promise((resolve, reject) => {
+ if (typeof GM_xmlhttpRequest !== 'function') {
+ reject(new Error('GM_xmlhttpRequest unavailable'));
+ return;
+ }
+
+ GM_xmlhttpRequest({
+ method: 'GET',
+ url,
+ responseType: 'arraybuffer',
+ onload: (res) => {
+ if (res.status < 200 || res.status >= 400 || !res.response) {
+ reject(new Error(`GM request failed (${res.status})`));
+ return;
+ }
+
+ const ctMatch = /content-type:\s*([^\r\n;]+)/i.exec(res.responseHeaders || '');
+ const mime = ctMatch ? ctMatch[1].trim() : 'application/octet-stream';
+ resolve(new Blob([res.response], { type: mime }));
+ },
+ onerror: () => reject(new Error('GM request network error')),
+ });
+ });
+ }
+
+ async function toDataUrl(src) {
+ const normalized = normalizeUrl(src);
+ if (!normalized) return '';
+ if (normalized.startsWith('data:')) return normalized;
+
+ if (dataUrlCache.has(normalized)) return dataUrlCache.get(normalized);
+
+ const p = (async () => {
+ try {
+ const res = await fetch(normalized);
+ if (!res.ok) throw new Error(`fetch failed (${res.status})`);
+ const blob = await res.blob();
+ return await blobToDataUrl(blob);
+ } catch {
+ const blob = await gmFetchBlob(normalized);
+ return await blobToDataUrl(blob);
+ }
+ })().catch(() => normalized); // final fallback: keep original URL
+
+ dataUrlCache.set(normalized, p);
+ return p;
+ }
+
+ async function imageElToTierImage(imgEl) {
+ const rawSrc = imgEl.currentSrc || imgEl.getAttribute('src') || '';
+ return {
+ src: await toDataUrl(rawSrc),
+ title: getImageTitle(imgEl),
+ description: getImageDescription(imgEl),
+ };
+ }
+
+ async function buildTierlistFile() {
+ const { rows: rawRows, parent } = findBestRowSet();
+ if (!rawRows.length) {
+ throw new Error('Could not find tier rows in the page.');
+ }
+
+ const result = {
+ title: detectTitle(),
+ rows: [],
+ };
+
+ const usedRowEls = new Set();
+ let untiered = null;
+
+ for (const row of rawRows) {
+ usedRowEls.add(row.rowEl);
+ const imgs = await Promise.all(row.imgEls.map(imageElToTierImage));
+
+ if (UNTIERED_RE.test(row.name)) {
+ untiered = imgs;
+ } else {
+ result.rows.push({ name: row.name, imgs });
+ }
+ }
+
+ if (!untiered) {
+ const root = parent || document.body;
+ const untieredEls = findUntieredElements(root, usedRowEls);
+ if (untieredEls.length) {
+ untiered = await Promise.all(untieredEls.map(imageElToTierImage));
+ }
+ }
+
+ if (untiered && untiered.length) {
+ result.untiered = untiered;
+ }
+
+ return result;
+ }
+
+ function downloadJson(filename, text) {
+ const blob = new Blob([text], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ a.remove();
+ URL.revokeObjectURL(url);
+ }
+
+ function slugify(name) {
+ return (name || 'tierlist')
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, '-')
+ .replace(/(^-|-$)/g, '') || 'tierlist';
+ }
+
+ async function exportNow() {
+ const data = await buildTierlistFile();
+ const json = JSON.stringify(data, null, 2);
+
+ console.log('Tierlist JSON:', data);
+ downloadJson(`${slugify(data.title)}.json`, json);
+
+ if (navigator.clipboard?.writeText) {
+ navigator.clipboard.writeText(json).catch(() => {});
+ }
+
+ return data;
+ }
+
+ // Expose manual API for console use:
+ window.exportTierlistJson = exportNow;
+
+ // Small on-page button:
+ const btn = document.createElement('button');
+ btn.textContent = 'Export Tierlist JSON';
+ btn.style.position = 'fixed';
+ btn.style.right = '16px';
+ btn.style.bottom = '16px';
+ btn.style.zIndex = '999999';
+ btn.style.padding = '8px 12px';
+ btn.style.border = '1px solid #444';
+ btn.style.borderRadius = '8px';
+ btn.style.background = '#111';
+ btn.style.color = '#fff';
+ btn.style.cursor = 'pointer';
+
+ btn.addEventListener('click', async () => {
+ const original = btn.textContent;
+ btn.disabled = true;
+ btn.textContent = 'Exporting...';
+ try {
+ await exportNow();
+ btn.textContent = 'Exported ✔';
+ } catch (err) {
+ console.error(err);
+ btn.textContent = 'Export failed';
+ } finally {
+ setTimeout(() => {
+ btn.disabled = false;
+ btn.textContent = original;
+ }, 1800);
+ }
+ });
+
+ document.body.appendChild(btn);
+})();
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage