AquaWolfGuy / AniDB add-episode button

// ==UserScript==
// @name        AniDB add-episode button
// @namespace   AquaWolfGuy
// @description Adds a button next to MyList entries and episodes for adding a new episode.
// @icon        https://static.anidb.net/css/icons/touch/apple-touch-icon.png
// @author      AquaWolfGuy
// @copyright   2018, AquaWolfGuy
// @license     GPL-3.0-only
// @match       *://anidb.net/*
// @version     1.0.6
// @grant       none
// ==/UserScript==


/////////////////
// Style sheet //
/////////////////

const style = document.createElement("style");
style.innerHTML = `
	#layout-main div.mylist_all table.animelist > tbody > tr > td.action {
		min-width: 8.5em;
	}
	@media screen and (min-width: 1640px) {
		#layout-main div.mylist_all table.animelist > tbody > tr > td.action {
			min-width: 14.5em;
		}
	}
	#layout-main div.anime_all div.episodes table.eplist td.action.episode {
		min-width: 13.5em;
	}
	.i_file_add::after {
		content: "\\f319";
		font-weight: normal;
	}
`;
document.head.appendChild(style);


////////////
// MyList //
////////////

const animelist = document.getElementById("animelist");
if (animelist !== null && document.body.classList.contains("mylist")) {

	new MutationObserver(preserveMyListButton).observe(animelist.tBodies[0], {childList: true});

	for (const row of animelist.querySelectorAll("#animelist>tbody>tr")) {
		addButtonToMyListEntry(row);
	}

	function addButtonToMyListEntry(row) {
		const aid = row.dataset.anidbAid;
		if (aid === undefined) {
			return;
		}
		const epsCell = row.getElementsByClassName("eps")[0];
		if (epsCell !== undefined) {
			const epsText = epsCell.textContent.trim();
			const epsParsed = /([0-9]+)\/([0-9]+)/.exec(epsText);
			if (epsParsed !== null && epsParsed[1] === epsParsed[2]) {
				return;
			}
		}
		const action = row.getElementsByClassName("action")[0];
		const button = document.createElement("a");
		button.className = "i_icon i_file_add";
		button.title = "add a file for the next episode";
		button.innerHTML = "<span>add file</span>";
		button.addEventListener("click", addFileByAid.bind(null, aid));
		button.addEventListener("auxclick", addFileByAid.bind(null, aid));
		button.addEventListener("mousedown", preventAutoScroll);
		action.appendChild(button);
	}

	function preserveMyListButton(mutationRecords, _mutationObserver) {
		for (const mutationRecord of mutationRecords) {
			for (const node of mutationRecord.addedNodes) {
				if (node instanceof Element && node.tagName === "TR") {
					addButtonToMyListEntry(node);
				}
			}
		}
	}

	function addFileByAid(aid, event) {
		if (!(event.button === 0 || event.button === 1)) {
			return;
		}
		const newTab = event.button === 1 || event.ctrlKey || event.shiftKey;

		const xhr = new XMLHttpRequest();
		xhr.responseType = "document";
		xhr.open("GET", "https://anidb.net/anime/" + aid);
		xhr.addEventListener("load", addFileXhrCallback.bind(null, xhr, newTab));
		xhr.addEventListener("error", xhrErrorCallback.bind(null, xhr));
		xhr.send();

		let loading = document.getElementById("loading");
		if (loading === null) {
			loading = document.createElement("div");
			loading.id = "loading";
			document.body.appendChild(loading);
		}
		loading.classList.add("active");

		event.preventDefault();
	}

	function addFileXhrCallback(xhr, newTab, _event) {
		const loading = document.getElementById("loading");
		if (loading !== null) {
			document.body.removeChild(loading);
		}

		if (xhr.status !== 200) {
			window.alert("Received \u201C" + xhr.status + " " + xhr.statusText + "\u201D response when loading episode list.");
			return;
		}

		let hasEps = false;
		let nextEpRow = null;
		for (const row of [...xhr.response.querySelectorAll("#eplist>tbody>tr")].reverse()) {
			const epNumber = row.getElementsByClassName("eid")[0].textContent.trim();
			if (!("0" <= epNumber[0] && epNumber[0] <= "9")) {
				 continue;
			}
			hasEps = true;
			const isAdded = row.getElementsByClassName("i_general_add").length === 0;
			if (isAdded) {
				break;
			}
			nextEpRow = row;
		}
		if (!hasEps) {
			window.alert("The anime entry does not have any normal episodes.");
			return;
		}
		if (nextEpRow === null) {
			window.alert("The last episode is in your MyList.");
			return;
		}
		const eid = nextEpRow.dataset.anidbEid;
		const uri = "https://anidb.net/file/add/?eid=" + eid;
		if (!newTab || window.open(uri) === null) {
			location.assign(uri);
		}
	}

	function xhrErrorCallback() {
		const loading = document.getElementById("loading");
		if (loading !== null) {
			document.body.removeChild(loading);
		}
		alert("Failed to load episodes list.");
	}

	function preventAutoScroll(event) {
		if (event.button === 1) {
			event.preventDefault();
		}
	}

}


////////////////
// Anime page //
////////////////

const eplist = document.getElementById("eplist");
if (eplist !== null) {

	for (const row of eplist.querySelectorAll("tbody>tr")) {
		addButtonToEpisodeEntry(row);
	}

	function addButtonToEpisodeEntry(row) {
		const eid = row.dataset.anidbEid;
		const action = row.querySelector(".action.episode");
		const button = document.createElement("a");
		button.className = "i_icon i_file_add";
		button.title = "add a new file";
		button.innerHTML = "<span>add new file</span>";
		button.href = "https://anidb.net/file/add/?eid=" + eid;
		action.insertBefore(button, action.firstElementChild);

		new MutationObserver(preserveEpisodeButton.bind(null, row)).observe(action, {childList: true});
	}

	function preserveEpisodeButton(row, mutationRecords, mutationObserver) {
		for (const mutationRecord of mutationRecords) {
			for (const node of mutationRecord.removedNodes) {
				if (node instanceof Element && node.classList.contains("i_file_add")) {
					mutationObserver.disconnect();
					addButtonToEpisodeEntry(row);
					return;
				}
			}
		}
	}

}