Pixiv Utils

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

目前為 2024-09-29 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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