Pixiv Utils

Utilities for Pixiv (tag block/highlight, adblock, ease-of-access, automation, premium emulation)

As of 2024-09-29. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name		Pixiv Utils
// @namespace	https://greasyfork.org/en/scripts/510779-pixiv-utils/
// @version		2024-09-29
// @description	Utilities for Pixiv (tag block/highlight, adblock, ease-of-access, automation, premium emulation)
// @author		V.H.
// @copyright	V.H.
// @match		*://*.pixiv.net/*
// @icon		https://icons.duckduckgo.com/ip2/pixiv.net.ico
// @grant		unsafeWindow
// @grant		GM_log
// @grant		GM_registerMenuCommand
// @grant		GM_unregisterMenuCommand
// @grant		GM_addStyle
// @grant		GM_getValue
// @grant		GM_setValue
// @run-at		document-start
// @tag			utilities
// @connect		self
// @webRequest	[{"selector":"ads-pixiv.net","action":"cancel"}]
// @license		MIT
// @supportURL	https://greasyfork.org/en/scripts/510779-pixiv-utils/feedback
// ==/UserScript==

"use strict";

const	sep			= /[\s\n]*[,\n][\s\n]*/gmis,
		idsep		= /\//gi,
		idmatch		= /^\d+/i,
		showc		= /show all|reading works?/i,
		prems		= [
			[ /(?<=['"]_setCustomVar['"]\s*?,\s*?\d+?\s*?,\s*?['"]plan['"]\s*?,\s*?['"])(normal)(?=['"])/gmis, "premium" ],
			[ /(?<=['"]?premium['"]?\s*?:\s*?['"])(no)(?=['"])/gmis, "yes" ],
			[ /(?<=['"]?plan['"]?\s*?:\s*?['"])(normal)(?=['"])/gmis, "premium" ],
			[ /(?<=['"]?premium['"]?\s*?:\s*?['"]?)(false|0|no)(?=['"]?)/gmis, "true" ],
		];
let		cfg			= {
	name:		"Pixiv Utils",
	prefix:		"gm_vh_pxvutl_",
	base:		"https://www.pixiv.net/en/artworks/",
	intr:		1000,
	baseopts:	{
		
	},
	panel:		{
		name:					"Panel",
		title:					"Panel",
		accessKey:				"P",
		autoClose:				true,
		id:						null,
	},
	observe:	{
		subtree:				true,
		childList:				true,
		characterData:			false,
		characterDataOldValue:	false,
		attributes:				true,
		attributeOldValue:		false,
		attributeFilter:		[ "src", "alt", "title" ],
	},
	style:		`
		.gm_vh_pxvutl_ {
			user-select: contain;
			text-shadow: 1px 1px 1px black;
		}
		.gm_vh_pxvutl_.row {
			display: flex;
			flex-flow: row wrap;
			justify-content: center;
			align-items: stretch;
			align-content: stretch;
			gap: 5px;
		}
		div:has(> div[width][height] .gm_vh_pxvutl_violet), div[type][size]:has(.gm_vh_pxvutl_violet) {
			border: medium dashed violet !important;
		}
		div:has(> div[width][height] .gm_vh_pxvutl_high), div[type][size]:has(.gm_vh_pxvutl_high) {
			border: medium dashed yellow !important;
		}
		div:has(> div[width][height] .gm_vh_pxvutl_green), div[type][size]:has(.gm_vh_pxvutl_green) {
			border: medium dashed green !important;
		}
		.gm_vh_pxvutl_block:not(.gm_vh_pxvutl_blue) {
			visibility: collapse !important;
			cursor: not-allowed;
		}
		div:has(> div[width][height] .gm_vh_pxvutl_block), div[type][size]:has(.gm_vh_pxvutl_block) {
			border: thick double red !important;
		}
		div:has(> div[width][height] .gm_vh_pxvutl_blue), div[type][size]:has(.gm_vh_pxvutl_blue) {
			visibility: visible !important;
			border-width: thick !important;
			border-style: dashed;
			border-color: blue !important;
		}
		.gm_vh_pxvutl_hide, div:has(> div[width][height] .gm_vh_pxvutl_hide), div[type][size]:has(.gm_vh_pxvutl_hide), div:has(> div > div[width][height] .gm_vh_pxvutl_hide, > div[type][size] .gm_vh_pxvutl_hide), li:has(> div > div > div[width][height] .gm_vh_pxvutl_hide, > div > div[type][size] .gm_vh_pxvutl_hide) {
			display: none !important;
			visibility: hidden !important;
			opacity: 0 !important;
			pointer-events: none !important;
			cursor: not-allowed !important;
		}
		
		:where(h1, h2, h3, h4).gm_vh_pxvutl_ {
			text-decoration: underline;
			margin: 5px;
		}
		label.gm_vh_pxvutl_ {
			user-select: none;
		}
		label.gm_vh_pxvutl_::after {
			content: ": ";
		}
		
		textarea.gm_vh_pxvutl_ {
			resize: both;
			border-radius: 5px;
			padding: 5px;
			margin: 5px;
			background: revert;
		}
		
		.gm_vh_pxvutl_:is(button, input, select) {
			cursor: pointer !important;
			opacity: .8;
			padding: 5px;
			margin: 5px;
			box-shadow: 1px 1px 1px 0 black;
			border-radius: 5px;
			background: revert;
			transition: all 200ms;
		}
		.gm_vh_pxvutl_:is(button, input, select):hover {
			box-shadow: 2px 2px 1px 1px black;
			opacity: .9;
			padding: 7px;
			border-radius: 7px;
		}
		.gm_vh_pxvutl_:is(button, input, select):active {
			box-shadow: 2px 2px 2px 2px black;
			opacity: 1;
			border-radius: 10px;
			padding: 8px;
		}
		
		#gm_vh_pxvutl_panel {
			display: flex;
			flex-flow: column nowrap;
			position: fixed !important;
			justify-content: space-between;
			align-items: stretch;
			align-content: stretch;
			resize: both !important;
			gap: 5px;
			bottom: 1vh !important;
			left: 1vw !important;
			min-width: 10vw;
			width: 30vw;
			max-width: 40vw;
			min-height: 10vh;
			height: 95vh;
			max-height: 100vh;
			padding: 5px;
			margin: 5px;
			border-radius: 5px;
			z-index: 999 !important;
			background: radial-gradient(circle farthest-side at center, rgba(230, 230, 230, 1) 0%, rgba(150, 150, 150, .7) 90%);
			overflow: auto;
			user-select: contain;
			pointer-events: none;
		}
		#gm_vh_pxvutl_panel * {
			pointer-events: auto;
		}
		#gm_vh_pxvutl_panel_close {
			position: sticky;
			top: 5px;
			right: 5px;
		}
	`,
},		data		= {
	enabled:	false,
	exposed:	true,
	show:		true,
	violet:		"",
	high:		"",
	green:		"",
	block:		"",
	blue:		"",
	hide:		"",
},		css			= GM_addStyle(cfg.style),
		observer	= new MutationObserver(see),
		tags		= {
			violet:	[ ],
			high:	[ ],
			green:	[ ],
			block:	[ ],
			blue:	[ ],
			hide:	[ ],
		},
		intr		= null;

document.addEventListener("DOMContentLoaded", premiumUnlock, true);

function start() {
	cfg.panel.id	= GM_registerMenuCommand(cfg.panel.name, panel, cfg.panel);
	
	data			= Object.assign(data, GM_getValue(getPrefixed("settings"), data));
	
	update();
	
	GM_log(`--- '${cfg.name}' has started.`);
} //start
function stop() {
	GM_unregisterMenuCommand(cfg.panel.id);
	css.remove();
	clearInterval(intr);
} //stop

function panel(e) {
	const	root		= document.createElement("dialog"),
			title		= document.createElement("h3"),
			close		= document.createElement("button"),
			statesp		= document.createElement("span"),
			statelb		= document.createElement("label"),
			state		= document.createElement("input"),
			expsp		= document.createElement("span"),
			explb		= document.createElement("label"),
			exp			= document.createElement("input"),
			showsp		= document.createElement("span"),
			showlb		= document.createElement("label"),
			show		= document.createElement("input"),
			violetsp	= document.createElement("span"),
			violetlb	= document.createElement("label"),
			violet		= document.createElement("textarea"),
			highsp		= document.createElement("span"),
			highlb		= document.createElement("label"),
			high		= document.createElement("textarea"),
			greensp		= document.createElement("span"),
			greenlb		= document.createElement("label"),
			green		= document.createElement("textarea"),
			blocksp		= document.createElement("span"),
			blocklb		= document.createElement("label"),
			block		= document.createElement("textarea"),
			bluesp		= document.createElement("span"),
			bluelb		= document.createElement("label"),
			blue		= document.createElement("textarea"),
			hidesp		= document.createElement("span"),
			hidelb		= document.createElement("label"),
			hide		= document.createElement("textarea");
	
	root.id				= getPrefixed("panel");
	root.classList.add(getPrefixed());
	root.setAttribute("open", "");
	
	{
		const	e	= document.getElementById(root.id);
		
		if (e)	e.remove();
	}
	
	title.id			= getPrefixed("panel_title");
	title.classList.add(getPrefixed());
	title.innerHTML		= "Pixiv Utils Control Panel";
	
	close.id			= getPrefixed("panel_close");
	close.classList.add(getPrefixed());
	close.innerHTML		= "Close";
	close.onclick		= () => {
		const	root	= document.getElementById(getPrefixed("panel"));
		
		root.close();
		root.remove();
	};
	
	state.id			= getPrefixed("panel_enable");
	state.type			= "checkbox";
	state.classList.add(getPrefixed());
	state.onchange		= () => {
		data.enabled	= state.checked;
		update();
	};
	if (data.enabled)	state.setAttribute("checked", "");
	
	statelb.classList.add(getPrefixed());
	statelb.htmlFor		= state.id;
	statelb.innerHTML	= "Enable";
	
	exp.id			= getPrefixed("panel_expose");
	exp.type			= "checkbox";
	exp.classList.add(getPrefixed());
	exp.onchange		= () => {
		data.exposed	= exp.checked;
		update();
	};
	if (data.exposed)	exp.setAttribute("checked", "");
	
	explb.classList.add(getPrefixed());
	explb.htmlFor		= exp.id;
	explb.title			= "The Alt field of images contains the tags";
	explb.innerHTML		= "Expose Alt";
	
	show.id				= getPrefixed("panel_show");
	show.type			= "checkbox";
	show.classList.add(getPrefixed());
	show.onchange		= () => {
		data.show		= show.checked;
		update();
	};
	if (data.show)		show.setAttribute("checked", "");
	
	showlb.classList.add(getPrefixed());
	showlb.htmlFor		= show.id;
	showlb.innerHTML	= "Auto Show-All";
	
	violet.id			= getPrefixed("panel_violet");
	violet.classList.add(getPrefixed());
	violet.value		= data.violet;
	violet.cols			= 50;
	violet.rows			= 3;
	violet.placeholder	= "tag1, tag2, ...";
	violet.setAttribute("spellcheck", "false");
	violet.onchange		= () => {
		data.violet		= violet.value;
		update();
	};
	
	violetlb.classList.add(getPrefixed());
	violetlb.htmlFor	= violet.id;
	violetlb.innerHTML	= "Violet Tags";
	
	high.id				= getPrefixed("panel_high");
	high.classList.add(getPrefixed());
	high.value			= data.high;
	high.cols			= 50;
	high.rows			= 3;
	high.placeholder	= "tag1, tag2, ...";
	high.setAttribute("spellcheck", "false");
	high.onchange		= () => {
		data.high		= high.value;
		update();
	};
	
	highlb.classList.add(getPrefixed());
	highlb.htmlFor		= high.id;
	highlb.innerHTML	= "Highlighted Tags";
	
	green.id			= getPrefixed("panel_green");
	green.classList.add(getPrefixed());
	green.value			= data.green;
	green.cols			= 50;
	green.rows			= 3;
	green.placeholder	= "tag1, tag2, ...";
	green.setAttribute("spellcheck", "false");
	green.onchange		= () => {
		data.green		= green.value;
		update();
	};
	
	greenlb.classList.add(getPrefixed());
	greenlb.htmlFor		= green.id;
	greenlb.innerHTML	= "Green Tags";
	
	block.id			= getPrefixed("panel_block");
	block.classList.add(getPrefixed());
	block.value			= data.block;
	block.cols			= 50;
	block.rows			= 3;
	block.placeholder	= "tag1, tag2, ...";
	block.setAttribute("spellcheck", "false");
	block.onchange		= () => {
		data.block		= block.value;
		update();
	};
	
	blocklb.classList.add(getPrefixed());
	blocklb.htmlFor		= block.id;
	blocklb.innerHTML	= "Blocked Tags";
	
	blue.id			= getPrefixed("panel_blue");
	blue.classList.add(getPrefixed());
	blue.value			= data.blue;
	blue.cols			= 50;
	blue.rows			= 3;
	blue.placeholder	= "tag1, tag2, ...";
	blue.setAttribute("spellcheck", "false");
	blue.onchange		= () => {
		data.blue		= blue.value;
		update();
	};
	
	bluelb.classList.add(getPrefixed());
	bluelb.htmlFor		= blue.id;
	bluelb.innerHTML	= "Blue Tags";
	
	hide.id				= getPrefixed("panel_hide");
	hide.classList.add(getPrefixed());
	hide.value			= data.hide;
	hide.cols			= 50;
	hide.rows			= 3;
	hide.placeholder	= "tag1, tag2, ...";
	hide.setAttribute("spellcheck", "false");
	hide.onchange		= () => {
		data.hide		= hide.value;
		update();
	};
	
	hidelb.classList.add(getPrefixed());
	hidelb.htmlFor		= hide.id;
	hidelb.innerHTML	= "Hide Tags";
	
	title.classList.add(getPrefixed(), "row");
	statesp.classList.add(getPrefixed(), "row");
	expsp.classList.add(getPrefixed(), "row");
	showsp.classList.add(getPrefixed(), "row");
	blocksp.classList.add(getPrefixed(), "row");
	highsp.classList.add(getPrefixed(), "row");
	greensp.classList.add(getPrefixed(), "row");
	bluesp.classList.add(getPrefixed(), "row");
	violetsp.classList.add(getPrefixed(), "row");
	hidesp.classList.add(getPrefixed(), "row");
	
	statesp.append(statelb, state);
	expsp.append(explb, exp);
	showsp.append(showlb, show);
	violetsp.append(violetlb, violet);
	highsp.append(highlb, high);
	greensp.append(greenlb, green);
	blocksp.append(blocklb, block);
	bluesp.append(bluelb, blue);
	hidesp.append(hidelb, hide);
	root.append(title, close, violetsp, highsp, greensp, blocksp, bluesp, hidesp, expsp, showsp, statesp);
	document.body.appendChild(root);
	
	root.show();
	
	GM_log(`--- '${cfg.name}' Panel opened.`);
} //panel

function update() {
	GM_setValue(getPrefixed("settings"), data);
	
	tags.violet	= data.violet.trim().split(sep).map(t => t.trim()).filter(t => t).map(r => new RegExp(wrap(r, "\\b"), "i"));
	tags.high	= data.high.trim().split(sep).map(t => t.trim()).filter(t => t).map(r => new RegExp(wrap(r, "\\b"), "i"));
	tags.green	= data.green.trim().split(sep).map(t => t.trim()).filter(t => t).map(r => new RegExp(wrap(r, "\\b"), "i"));
	tags.block	= data.block.trim().split(sep).map(t => t.trim()).filter(t => t).map(r => new RegExp(wrap(r, "\\b"), "i"));
	tags.blue	= data.blue.trim().split(sep).map(t => t.trim()).filter(t => t).map(r => new RegExp(wrap(r, "\\b"), "i"));
	tags.hide	= data.hide.trim().split(sep).map(t => t.trim()).filter(t => t).map(r => new RegExp(wrap(r, "\\b"), "i"));
	
	if (data.enabled)	observer.observe(document, cfg.observe);
	else				observer.disconnect();
	if (data.show)		intr	= setInterval(timed, cfg.intr);
	else				clearInterval(intr);
	
	GM_log("Update.");
} //update

function timed() {
	const	show	= Array.from(document.querySelectorAll("button:not(.hidden)")).find(b => showc.test(b.innerText.trim().toLowerCase()));
	
	if (show) {
		show.click();
		show.classList.add("hidden");
	}
} //timed

start();

function premiumUnlock() {
	const	scripts	= Array.from(document.scripts).filter(s => s.innerText.trim()),
			metas	= Array.from(document.querySelectorAll("meta[content]")).filter(m => m.content.trim());
	
	GM_log(`PREMS:\t${prems.length}\t${scripts.length}\t${metas.length}`);
	
	for (let script of scripts) {
		prems.forEach(p => {
			script.innerText	= script.innerText.replaceAll(p[0], p[1]);
		});
	}
	for (let meta of metas) {
		prems.forEach(p => {
			meta.content	= meta.content.replaceAll(p[0], p[1]);
		});
	}
	
	GM_log(`--- '${cfg.name}' has premium-unlocked.`);
} //premiumUnlock

function see(e, o) {
	for (let mut of e) {
		mut.addedNodes && mut.addedNodes.forEach(nd => {
			if (nd.tagName && nd.tagName.toLowerCase() == "img" && nd.alt)	process(nd);
		});
	}
} //see
function process(img, skip = false) {
	GM_log(`IMG:\t${img.src}\t${img.alt}`);
	
	const	alt	= img.alt.trim().toLowerCase().split(sep).map(t => t.trim()).filter(r => r);
	
	if (rule(tags.violet, alt)) {
		img.classList.add(getPrefixed("violet"));
		GM_log(`VIOLET:\t${img.src}\t${img.alt}`);
	}
	if (rule(tags.high, alt)) {
		img.classList.add(getPrefixed("high"));
		GM_log(`HIGH:\t${img.src}\t${img.alt}`);
	}
	if (rule(tags.green, alt)) {
		img.classList.add(getPrefixed("green"));
		GM_log(`GREEN:\t${img.src}\t${img.alt}`);
	}
	if (rule(tags.block, alt)) {
		img.classList.add(getPrefixed("block"));
		GM_log(`BLOCK:\t${img.src}\t${img.alt}`);
	}
	if (rule(tags.blue, alt)) {
		img.classList.add(getPrefixed("blue"));
		GM_log(`BLUE:\t${img.src}\t${img.alt}`);
	}
	if (rule(tags.hide, alt)) {
		img.classList.add(getPrefixed("hide"));
		GM_log(`HIDE:\t${img.src}\t${img.alt}`);
	}
	
	if (data.exposed)	img.parentNode.title	= img.title	= img.alt.trim();
	if (skip)	return;
	
	getData(img).then(tags => {
		img.alt	+= ", " + tags.join(", ");
		
		process(img, true);
	});
} //process

async function getData(img) {
	try {
		const	id		= getID(img.src),
				res		= await fetch(cfg.base + id, cfg.baseopts),
				dat		= await res.text(),
				tree	= Document.parseHTMLUnsafe(dat).querySelector("meta[name='preload-data']"),
				json	= JSON.parse(tree.content)["illust"][id]["tags"]["tags"],
				tags	= [ ];
		
		for (let tag of json) {
			if (tag["tag"])								tags.push(tag["tag"]);
			if (tag["romaji"])							tags.push(tag["romaji"]);
			if (tag["translation"])
				for (let trans in tag["translation"])	tags.push(tag["translation"][trans]);
		}
		
		return tags;
	} catch(err) {
		return [ ];
	}
} //getData

function rule(r = [], t = []) {
	return r.reduce((acc, curr) => acc + t.reduce((acc, curt) => acc + curr.test(curt), 0), 0);
} //rule

function getID(src) {
	return src.trim().split(idsep).map(i => i.trim()).filter(i => i).pop().match(idmatch)[0];
} //getID

function getPrefixed(thing = "") {
	return cfg.prefix.trim() + thing.trim();
} //getPrefixed
function wrap(thing = "", w = "") {
	return w + thing + w;
} //wrap