Kemono Browser

Adds a button at the bottom right of all kemono, coomer & pawchive supported creator websites that redirects to the corresponding page.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

// ==UserScript==
// @name        Kemono Browser
// @namespace   Violentmonkey Scripts
// @version     1.10.1
// @description Adds a button at the bottom right of all kemono, coomer & pawchive supported creator websites that redirects to the corresponding page.
// @author      zWolfrost
// @license     MIT
// @match       *://*.patreon.com/*
// @match       *://*.fanbox.cc/*
// @match       *://*.pixiv.net/*
// @match       *://*.discord.com/*
// @match       *://*.fantia.jp/*
// @match       *://*.boosty.to/*
// @match       *://*.dlsite.com/*
// @match       *://*.gumroad.com/*
// @match       *://*.subscribestar.com/*
// @match       *://*.subscribestar.adult/*
// @match       *://*.onlyfans.com/*
// @match       *://*.fansly.com/*
// @match       *://*.candfans.com/*
// @connect     kemono.cr
// @connect     coomer.st
// @connect     pawchive.st
// @connect     fansly.com
// @icon        https://kemono.cr/static/favicon.ico
// @grant       GM.xmlHttpRequest
// @grant       GM.getResourceUrl
// @grant       GM.openInTab
// @noframes
// ==/UserScript==
"use strict";


///////////////// OPTIONS /////////////////

// Buttons to include
const BUTTONS = {
	PAWCHIVE: true,
	KEMONO: true,
	COOMER: true
}

// Whether to open the url in a new tab by default
// Note that ctrl+clicking the button does the opposite of the default behavior
const OPEN_IN_NEW_TAB = true;

// Button classes to apply
const BUTTONS_CLASSES = {
	"include-icon": true,
	"include-text": false,
	"animate-click": true
}

// Buttons CSS
const BUTTONS_CSS = `
#_kemono-browser-container {
	--status-prefix: "Creator: ";
}

/* WEBSITE-SPECIFIC BUTTON CSS */
#_kemono-browser-container.discord { transform: translate(-248px, -75px); }
#_kemono-browser-container.patreon { --btn-shadow-color: #ffffff50; }
#_kemono-browser-container.x { --btn-shadow-color: #ffffff50; }


#_kemono-browser-container {
	--btn-shadow-color: transparent;

	display: block !important;
	position: fixed !important;

	z-index: 999999 !important;
	right: 0 !important;
	bottom: 0 !important;
}


#_kemono-browser-container > a {
	display: none !important;
	position: relative !important;

	min-width: 1vh !important;
	min-height: 1vh !important;
	max-width: max-content !important;
	max-height: max-content !important;

	align-items: center !important;
	justify-content: center !important;
	gap: 0.3vh !important;

	border: 0.2vh solid black !important;
	border-radius: 0.4vh !important;
	padding: 0.6vh !important;
	margin: 1.2vh !important;
	margin-left: auto !important;

	font-family: arial !important;
	font-weight: bold !important;
	font-size: 1.9vh !important;

	line-height: normal !important;
	text-decoration: none !important;
	cursor: pointer !important;
	user-select: none !important;

	transition-property: top, right, bottom, left, box-shadow !important;
	transition-duration: 0.05s !important;
	transition-timing-function: ease-out !important;
}
#_kemono-browser-container > a.disabled {
	pointer-events: none !important;
	opacity: 0.5 !important;
}
#_kemono-browser-container > a:hover { filter: brightness(90%); }
#_kemono-browser-container > a:active { filter: brightness(75%); }


#_kemono-browser-container > a > img {
	display: none !important;
}
#_kemono-browser-container > a.include-icon > img {
	display: inline-block !important;
	width: 2.3vh !important;
	height: 2.3vh !important;
}
#_kemono-browser-container > a.disabled[data-status=update] > img {
	animation: rotating 1s linear infinite !important;
}
@keyframes rotating {
	to { transform: rotate(360deg); }
}


#_kemono-browser-container > a.animate-click {
	bottom: 0.0vh;
	right: 0.0vh;
	box-shadow: black 0.05vh 0.05vh, black 0.1vh 0.1vh, black 0.15vh 0.15vh, black 0.2vh 0.2vh,
		black 0.25vh 0.25vh, black 0.3vh 0.3vh, black 0.35vh 0.35vh, black 0.4vh 0.4vh,
		var(--btn-shadow-color) 0vh 0vh 0.5vh, var(--btn-shadow-color) 0.4vh 0.4vh 0.5vh;
}
#_kemono-browser-container > a.animate-click:active {
	bottom: -0.4vh;
	right: -0.4vh;
	box-shadow: var(--btn-shadow-color) 0vh 0vh 0.5vh;
}


#_kemono-browser-container > a[data-status] { display: flex !important; background-color: #444444; color: white; }
#_kemono-browser-container > a.include-text[data-status]::after { content: var(--status-prefix) "Unknown (error " attr(data-status) ")"; }

#_kemono-browser-container > a[data-status=found] { background-color: green; color: white; }
#_kemono-browser-container > a.include-text[data-status=found]::after { content: var(--status-prefix) "Found"; }

#_kemono-browser-container > a[data-status=incomplete] { background-color: gold; color: black; }
#_kemono-browser-container > a[data-status=incomplete] > img { filter: invert(1); }
#_kemono-browser-container > a.include-text[data-status=incomplete]::after { content: var(--status-prefix) "Incomplete"; }

#_kemono-browser-container > a[data-status=missing] { background-color: red; color: white; }
#_kemono-browser-container > a.include-text[data-status=missing]::after { content: var(--status-prefix) "Missing"; }

#_kemono-browser-container > a[data-status=pending] { background-color: gray; color: white; }
#_kemono-browser-container > a.include-text[data-status=pending]::after { content: var(--status-prefix) "Pending..."; }

#_kemono-browser-container > a[data-status=update] { background-color: gray; color: white; }
#_kemono-browser-container > a[data-status=update]::after { display: none; }
#_kemono-browser-container > a.disabled[data-status=update]::after { display: inline; content: "Waiting to avoid hitting rate-limit..."; }
`;


////////////// BUTTONS STUFF //////////////

// initialize buttons
function initButtons() {
	// get domain & classes to include
	const domain = window.location.hostname.split(".").slice(-2).join(".");

	// append css to head
	document.head.appendChild(document.createElement("style")).innerHTML = BUTTONS_CSS;

	// create button container
	const BUTTONS_CONTAINER = document.createElement("div");
	BUTTONS_CONTAINER.id = "_kemono-browser-container";
	BUTTONS_CONTAINER.classList.add(domain.split(".")[0]);
	document.body.prepend(BUTTONS_CONTAINER);

	// create update button
	BUTTONS.UPDATE = true;

	for (let key in BUTTONS) {
		if (BUTTONS[key]) {
			// create button element
			BUTTONS[key] = document.createElement("a");

			// set button icon
			let name = key.toLocaleLowerCase();
			BUTTONS[key].id = `_${name}-btn`;
			const ICON = document.createElement("img");
			ICON.src = `data:image/png;base64,${ICONS_B64[name]}`;
			BUTTONS[key].prepend(ICON);

			// set button attributes
			let classes = Object.keys(BUTTONS_CLASSES).filter(key => BUTTONS_CLASSES[key]);
			BUTTONS[key].classList.add(...classes);
			BUTTONS[key].target = OPEN_IN_NEW_TAB ? "_blank" : "_self";
			BUTTONS[key].draggable = false;
			BUTTONS[key].querySelector("img").draggable = false;

			// add ctrl+click event listener
			BUTTONS[key].addEventListener("click", function(e) {
				if (e.ctrlKey) {
					e.preventDefault();

					if (this.target == "_self") GM.openInTab(this.href);
					else window.open(this.href, "_self");
				}
			});

			// append button to body
			BUTTONS_CONTAINER.prepend(BUTTONS[key]);
		}
		else {
			delete BUTTONS[key];
		}
	}

	if (domain in rateLimitedDomainMethods) {
		BUTTONS.UPDATE.classList.add("include-icon");
		BUTTONS.UPDATE.dataset.status = "update";
		BUTTONS.UPDATE.addEventListener("click", function() {
			rateLimitedDomainMethods[domain]().then(updateButtons)

			// wait a bit before re-enabling the button to avoid hitting the rate limit
			this.classList.add("disabled");
			setTimeout(() => this.classList.remove("disabled"), 3000);
		});
		delete BUTTONS.UPDATE;
	}
	else if (domain in domainMethods) {
		setInterval(() => {
			if (!document.getElementById(BUTTONS_CONTAINER.id)) document.body.prepend(BUTTONS_CONTAINER);
			updateButtons(domainMethods[domain]());
		}, 222);
	}
}

// update buttons
function updateButtons(urls) {
	for (let key in BUTTONS) {
		let newURL = urls[key] ?? "";
		if (newURL != BUTTONS[key].dataset.href) {
			if (newURL) {
				// set the button to the pending status, while waiting for a response
				BUTTONS[key].href = newURL;
				BUTTONS[key].dataset.href = newURL;
				BUTTONS[key].dataset.status = "pending";

				getCreatorStatus(BUTTONS[key].dataset.href).then(status => {
					if (status == "incomplete" && newURL.includes("/post")) {
						BUTTONS[key].href = newURL.split("/").slice(0, -2).join("/");
					}

					if (BUTTONS[key].dataset.href == newURL) {
						BUTTONS[key].dataset.status = status;
					}
				});
			}
			else {
				BUTTONS[key].href = "";
				delete BUTTONS[key].dataset.href;
				delete BUTTONS[key].dataset.status;
			}
		}
	}
}


////////// URLs EXTRACTION STUFF //////////

const domainMethods = {
	"patreon.com": extractURLFromPatreon,
	"fanbox.cc": extractURLFromFanbox,
	"pixiv.net": extractURLFromPixiv,
	"discord.com": extractURLFromDiscord,
	"fantia.jp": extractURLFromFantia,
	"boosty.to": extractURLFromBoosty,
	"dlsite.com": extractURLFromDlsite,
	"gumroad.com": extractURLFromGumroad,
	"subscribestar.com": extractURLFromSubscribeStar,
	"subscribestar.adult": extractURLFromSubscribeStar,
	"onlyfans.com": extractURLFromOnlyFans,
	"candfans.com": extractURLFromCandFans
}
const rateLimitedDomainMethods = {
	"fansly.com": extractURLFromFansly
}

// create the creator url with the given parameters
function compileURL({domains, service, userID=null, postID=null} = {}) {
	let obj = {};

	for (let domain of domains) {
		let redirectURL = `https://${domain}/${service}`;

		if (userID) {
			redirectURL += `/user/${userID}`;

			if (postID) {
				redirectURL += `/post/${postID}`;
			}
		}
		else continue;

		obj[domain.split(".").at(-2).toUpperCase()] = redirectURL;
	}

	return obj;
}

function extractURLFromPatreon() {
	return compileURL({
		domains: ["kemono.cr", "pawchive.st"],
		service: "patreon",
		userID: extract(select("#__NEXT_DATA__"), '"creator":{"data":{"id":"', '"') ??
			extract(select("body"), '\\"https://www.patreon.com/api/user/', "\\"),
		postID: extractNextUrlPath("posts")?.split("-")?.at(-1)
	})
}
function extractURLFromFanbox() {
	return compileURL({
		domains: ["kemono.cr", "pawchive.st"],
		service: "fanbox",
		userID: extract(select('meta[property="og:image"]', "content"), "/creator/", "/") ??
			extract(select(".styled__StyledUserIcon-sc-1upaq18-10[style]", "style"), "/user/", "/") ??
			extract(select('a[href^="https://www.pixiv.net/users/"]', "href"), "/users/", "/"),
		postID: extractNextUrlPath("posts")
	})
}
function extractURLFromPixiv() {
	return compileURL({
		domains: ["kemono.cr", "pawchive.st"],
		service: "fanbox",
		userID: extractNextUrlPath("users") ??
			select("button[data-gtm-user-id]", "data-gtm-user-id") ??
			select("a.user-details-icon[href]", "href")?.split("/").at(-1),
	})
}
function extractURLFromDiscord() {
	const pathname = window.location.pathname.split("/");

	const serverID = /\d/.test(pathname[2]) ? `${pathname[2]}/${pathname?.[3] ?? ""}` : null

	return serverID ? {
		"KEMONO": `https://kemono.cr/discord/server/${serverID}`
	} : {}
}
function extractURLFromFantia() {
	return compileURL({
		domains: ["kemono.cr"],
		service: "fantia",
		userID: extractNextUrlPath("fanclubs") ?? extract(select(".fanclub-header > a[href]", "href"), "/fanclubs/", "/"),
		postID: extractNextUrlPath("posts") ?? extractNextUrlPath("products")
	})
}
function extractURLFromBoosty() {
	return compileURL({
		domains: ["kemono.cr"],
		service: "boosty",
		userID: extractNextUrlPath("/"),
		postID: extractNextUrlPath("posts")
	})
}
function extractURLFromDlsite() {
	return compileURL({
		domains: ["kemono.cr"],
		service: "dlsite",
		userID: extractNextUrlPath("maker_id", ".html") ?? extract(select(".maker_name[itemprop=brand] > a", "href"), "/maker_id/", "."),
		postID: concatOrFalsy("RE", extractNextUrlPath("product_id")?.replace(/\D/g, ""))
	})
}
function extractURLFromGumroad() {
	const json = select("div#app[data-page]", "data-page");

	return compileURL({
		domains: ["kemono.cr"],
		service: "gumroad",
		userID: extract(json, '"external_id":"', '"') ?? extract(json, '"seller":{"id":"', '"'),
		postID: select('meta[property="product:retailer_item_id"]', "content")
	})
}
function extractURLFromSubscribeStar() {
	return compileURL({
		domains: ["kemono.cr"],
		service: "subscribestar",
		userID: select('img[data-type="avatar"][alt]', "alt").toLowerCase(),
		postID: extractNextUrlPath("posts")
	})
}
function extractURLFromOnlyFans() {
	return compileURL({
		domains: ["coomer.st"],
		service: "onlyfans",
		userID: select("#content .g-avatar[href]", "href")?.split("/")[1],
		postID: select("div.b-post:not(.is-not-post-page)", "id")?.replace(/\D/g, "")
	})
}
async function extractURLFromFansly() {
	let userID = null;

	const userName = select("div.feed-item-name a.username-wrapper", "href")?.split("/").at(-1) ?? select("meta[property='og:title']", "content")?.slice(10);
	if (userName) {
		const res = await request({ method: "GET", url: `https://apiv3.fansly.com/api/v1/account?usernames=${userName}&ngsw-bypass=true` });
		userID = JSON.parse(res.responseText)?.response?.[0].id ?? null;
	}

	return compileURL({
		domains: ["coomer.st"],
		service: "fansly",
		userID: userID,
		postID: extractNextUrlPath("post")
	})
}
function extractURLFromCandFans() {
	return compileURL({
		domains: ["coomer.st"],
		service: "candfans",
		userID: extract(select("meta[property='og:image']", "content"), "/user/", "/"),
		postID: extractNextUrlPath("show")
	})
}


////////////// UTILITY STUFF //////////////

/**
 * get query element attribute shorthand
 * @returns {string}
 */
function select(query, attribute=null) {
	const el = document.querySelector(query);
	return attribute ? el?.getAttribute(attribute) : el?.innerHTML
}

/**
 * get string between a prefix and a suffix
 * @returns {string}
 */
function extract(string, prefix, suffix) {
	if (string == null) return null;

	let begIndex = string.indexOf(prefix);
	if (begIndex == -1) return null;
	else begIndex += prefix.length;

	let endIndex = string.indexOf(suffix, begIndex);
	if (endIndex == -1) endIndex = undefined;
	let result = string.slice(begIndex, endIndex);

	return result;
}

/**
 * get next path segment in url pathname after a prefix.
 * if prefix is blank, return the first path segment.
 * @returns {string}
 */
function extractNextUrlPath(prefix, suffix="/") {
	return extract(window.location.pathname, (prefix == "/") ? "/" : `/${prefix}/`, suffix);
}

/**
 * concatenate strings if they are truthy, otherwise return the first falsy string
 * @returns {string}
 */
function concatOrFalsy(...args)
{
	for (let arg of args) {
		if (!arg) return arg;
	}
	return args.join("");
}

/**
 * check if the creator exists on kemono
 * @returns {string}
 */
async function getCreatorStatus(url) {
	if (url) {
		const Url = new URL(url);

		if (Url.hostname == "kemono.cr" || Url.hostname == "coomer.st") {
			if (Url.pathname.split("/")[1] == "discord") {
				const response = await request({ method: "GET", url: `https://${Url.hostname}/api/v1/discord/server/${Url.pathname.split("/")[3]}` });

				if (response.status == 200) {
					let channels = JSON.parse(response.responseText).channels;

					if (channels.length == 0) return "missing";
					else if (channels.some(channel => channel.id == Url.pathname.split("/").at(-1))) return "found";
					else return "incomplete";
				}
				else if (response.status == 404) return "missing";
				else return response.status;
			}
			else {
				const is_post = Url.pathname.includes("/post");

				if (is_post) {
					const postResponse = await request({ method: "HEAD", url: `https://${Url.hostname}/api/v1${Url.pathname}` });

					if (postResponse.status == 200 || postResponse.status == 202) return "found";
					Url.pathname = Url.pathname.split("/").slice(0, -2).join("/");
				}

				const response = await request({ method: "HEAD", url: `https://${Url.hostname}/api/v1${Url.pathname}/profile` });

				if (response.status == 200 || response.status == 202) return is_post ? "incomplete" : "found";
				else if (response.status == 404) return "missing";
				else return response.status;
			}
		}
		else if (Url.hostname == "pawchive.st") {
			const response = await request({ method: "HEAD", url: url });
			const redirectUrl = response?.finalUrl;

			if (response.status == 200) {
				if (redirectUrl == url) return "found";
				else if (redirectUrl.includes("user")) return "incomplete";
				else if (redirectUrl.includes("artists")) return "missing";
			}
			else return "missing";
		}
	}

	return 400;
}

/**
 * make a request
 * @returns {Promise}
 */
function request(details) {
	return new Promise((resolve, reject) => {
		GM.xmlHttpRequest({ ...details, headers: {"Accept": "text/css"}, onload: resolve, onerror: reject });
	});
}


/* base64 of icons */
let ICONS_B64 = {
	"kemono": "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAA4VBMVEUAAAD8///////////////////////+/v7////+/v7////////////////////////////////////////////////////////////////////////////////////////+/v7////////////+/v7+/v7+/v7+/v7+/v7////////+/v7////+/v7+/v7////////////////////////////////////+/v7+/v7////////+/v7+/v7+/v4B//7+/v7+/v7////+/v7+/v7////+Af/////+//7+/v7+/P4l5DYqAAAASHRSTlMAAgQHCg8SFhkbHiEkKCouMjY6PUJGSk9SVVpfZGpscHZ6foKEiYyPk5aanqOnq6+0uL3Aw8fKztLa3uLl5+np6+7z9vj7/P7TBRA8AAADyklEQVR42p3T6XaiShQF4A04dTROUeMUjSYa4xDnAaN9EeHau9//gRpqKaAhDvnW4gdFFWevU1VI4ueS6wFgvir4oUedVUDn4ochqiYZAzqk+aLA168cLOkIfEgtkksAGZLfhegyBUTZxVfBAS3vsCxJvxBhAE1+AHUOAEgn2ea05WF5oTBPwKum5+1PZhwqx0Bw1oZHckWbEYQlRv4VbzUZrgKNMipkJ0VOrfUcwpXTKUwgTMilScssDoe04K5WIo0+qVrrWThuP9UNWYNQJs36UoR4lj0RyCmF9YzUFOzJLVG8TTIJIWiQWrJH2zTuieDVcto/pKWfNUlNcnaLXIZL+nGIAr2SR+1vh1TS3d6cqB2Iz2ib3kMIb+jSYofTT8sLOrSUsCevaBnKyhttWztEqrul125UUMTpJ80nFMVQBAdN2roychrJ35xUZtzqPLFu1HYkt4+4F9/mcCR4+ENkRNafdKrVULhp0MfmAYE5bQ24ZhT6AUg1s4toRoLlrmvy1GcCaFPIwFWhIE59etlQnGgDHlvdAYE1bboCV9ig8Gwv2ngv58OEXmYewJNTzNWnkAYiS5JGXcZBXqWHngLitFXgKJWkPIXooUGc3eNALq/o0qJQaIvhQFlzktLElQzIA+5tq5I743lDhxqSDZJLCY6cSXNDm9yia3wHR9DZ0+3iWa4+po0iPBrce6eXXpbg+NUedWrFhzCEDLPYk+1nQn+DCPzVmcLe9HPUqpQN+tOK8NVlXFS/T+CD5/VC8DFnPFlqLgwugcyCZzXxhXSn0qBtUwEgVzY8Yy3jSLI2WJGC+R4GIMWfhjwnC6+wxoNJEpb+b17Qg4e3Z6teJSGBFxlBuAo8piFVrHenBs8pwxHSeEyHRX7lWWM4enQZzfJDRAIQGfO8XQx7eVL4S9skAltm7Qz6EMOTl3IuHgRCJw1fZwEoGv+fNYpbXrLTF6qniGnYY3UJeMyHgDce+8NLzHT2bW6qMoSITq5U3uQDQOjQnRY5iozI9YpXmQ1JMwnHnbl7kzEiqwVeZSS97jiEQ+kUAcy4DmDIiwbkCMjpjOGYyioQNXjBQl5QBXBfxIm5FQCo84IcMlThR/4Fi6LSzx/vuR51cUaWZyVx0QfP6OGyiM5vGVFcocpvveEa8pzf0EO4Stqkv2dcqU1fnwquFP6Pfkq4Wpk+ZhKuJo35VQY3SJg8NcBNmjxhJnCT4CePdXCjPI/od7jVkFy8Psai0WRlTDZws9i2j4OBFsDt6sYhdsSs4AcU9R0QXlQFP5HdR5DW+W/n/APmACehd2Z/OAAAAABJRU5ErkJggg==",
	"pawchive": "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",
	"update": "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAADcElEQVR42u3bMWgUWRzH8T/BFIJaaRUEwUCaYBe5gBKD3BXhDkECplCwES3uwDuE2K2NChYJKpwHtjZiwE4sBEUbi8MDwV7kDrEQAiK56O73axMkBN19OzvzspnkA1MMOzDsb9//v8N7b2LLlnzUfeo0MKs2XDlWzqfUXVEX6qifbKh3gc9257V6FTinDsZGoQ4DV4APlogmz9Tz6kD0I3VcfWAeN4G90aVKRpK6W72v+QG31R2RQJ1Q56JM6i/taxu15be1+4i256sBy004EW2oR1SB+SgLcEfToV9h+YCH6rZYQz2qWmoA6mP7EPBKHYkV6jHVUgNQn5hBy8IW1TH1lGqpAQCP3ACAJdVSAwAu27tFdU69qB74xj12wv/H1QawYLmKB6BOWhDQBK6powXvfVp9um4BqAPAW7sELAOzURL1sPo8ewDALbsELMCb7VEB9bdsAagjmg5AnYqKqYeApcoDAO6ZCPiojkcm6kFVpZoA1P0mAprqWGQE/FnpCFBvmG4yMgJ+r7wHAJjmemQEzFTeBIGzJoD4LzJSf1TNEcCCCYCTkYk6ke1ByATAu8hEHQZeAP/0eFwos8YaUUfqJRMAQ1FHwEs7AN5HXSXOTMxFHQF7Nnv9nzHNdNSR2rCTVo0boNowQfQ50zRqG0BrM/cxYGizN/Jp05yJOkotY2BP1JE6V4c+Vhjw3g6Al1FHwFDi6uKlqCO1kbLaCsykpHnh60RC83sTDM1OExAv1OHIBHhXWv0D8/ZuIjIBTqauWnUXQMuifoqMgH9NAJytfAQAM5GRet0EAJGmeADAH1FMjqX7G5UGAPwVGaljQNN0+6seAQcjE3Uc+Ggi4F4kSAyAdntzDmX48lMAdmekQADFdnQBv0YFeMP2InuHgFuxSq6/wefq4SgJMKt+skvAW3WgWADleKqeLrrdHrgGNLGwyaKJz1syYEFtAMeBnbGGekC9qM6pi/YIuBxp0gOgxZI9wuoBjyJZegCn1DF10f72JJKlB3Bs9e4x4JUdofk9jmTpARyNNdRtwEP7CHAnkqUHcKTDdSeA5fX87YHP6s9VTC5OJF67A7jt+riv7o6y6d+D0SVgr3rTPB6oP0Q/UgfU88AzSwR8AK6ow7FRqIPAOfWq+rpAbd9VG+po1IW6S536zuuz0+q+2LIlmy9UzYp4f2kiyQAAAABJRU5ErkJggg=="
}
ICONS_B64["coomer"] = ICONS_B64["kemono"]


initButtons();