From 062b7cd85b4c9715ad22dda5f1d89f2e2d5fd699 Mon Sep 17 00:00:00 2001 From: Pinapelz Date: Mon, 18 May 2026 03:16:58 -0700 Subject: add tiermaker export userscript --- tiermaker.user.js | 371 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 371 insertions(+) create mode 100644 tiermaker.user.js 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); +})(); -- cgit v1.2.3