Crunchyroll HTML5 Manga Viewer

Replaces Flash manga viewer with an HTML5 setup

Versión del día 29/5/2018. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name        Crunchyroll HTML5 Manga Viewer
// @namespace   DoomTay
// @description Replaces Flash manga viewer with an HTML5 setup
// @require     https://cdn.jsdelivr.net/npm/[email protected]/dist/vast-player.min.js
// @include     http://www.crunchyroll.com/comics*
// @version     0.6.0
// @grant       none
// ==/UserScript==

var mangaObject = document.querySelector("object#showmedia_videoplayer_object");

var mangaConfig = {};
var userID = null;
var pages = [];

var newStyle = document.createElement("style");
newStyle.innerHTML = `#mangaViewer
{
	background-color: rgb(224, 224, 224);
	display: flex;
	flex-direction: column;
	border: 10px solid rgb(153, 153, 153);
	box-sizing: border-box;
}

#mangaViewer:fullscreen
{
	width: 100%;
	height: 100% !important;
	border: 0px;
}

#mangaViewer:-moz-full-screen
{
	width: 100%;
	height: 100% !important;
	border: 0px;
}

#mangaViewer:-webkit-full-screen
{
	width: 100%;
	height: 100% !important;
	border: 0px;
}

#pageDisplay
{
	width: 100%;
	height: 100%;
	overflow: hidden;
	display: flex;
	flex: auto;
	justify-content: center;
}

.pageContainer
{
	height: 100%;
	width: 50%;
}

.mangaPage
{
	width: auto;
	height: 100%;
	border: 0px;
}

#controlBar
{
	background-color:#999;
	height: 100px;
	display: flex;
	width: 100%;
	flex: none;
}

#mangaViewer:fullscreen #pageDisplay
{
	margin-bottom: 25px;
}

#mangaViewer:-moz-full-screen #pageDisplay
{
	margin-bottom: 25px;
}

#mangaViewer:-webkit-full-screen #pageDisplay
{
	margin-bottom: 25px;
}

#mangaViewer:fullscreen #controlBar
{
	position: absolute;
	bottom: -75px;
	transition: 0.5s;
}

#mangaViewer:-moz-full-screen #controlBar
{
	position: absolute;
	bottom: -75px;
	transition: 0.5s;
}

#mangaViewer:-webkit-full-screen #controlBar
{
	position: absolute;
	bottom: -75px;
	transition: 0.5s;
}

#mangaViewer:fullscreen #controlBar:hover
{
	bottom: 0;
}

#mangaViewer:-moz-full-screen #controlBar:hover
{
	bottom: 0;
}

#mangaViewer:-webkit-full-screen #controlBar:hover
{
	bottom: 0;
}

#controlBar > *
{
	margin: auto;
}

#controlBar button
{
	width: 50px;
	height: 50px;
	font-size: 26px;
}`;
document.head.appendChild(newStyle);

var currentPage = 0;

if(mangaObject)
{
	var flashVars = parseFlashVars(mangaObject.querySelector("param[name='flashvars']").value);
	var authToken = flashVars.auth || null;

	var mangaDiv = document.createElement("div");
	mangaDiv.id = "mangaViewer";
	mangaDiv.style.width = mangaObject.width;
	mangaDiv.style.height = mangaObject.height + "px";

	var playerDiv = document.createElement("div");
	playerDiv.style.width = mangaObject.width;
	playerDiv.style.height = mangaObject.height + "px";

	mangaObject.parentNode.replaceChild(playerDiv,mangaObject);

	var pageL = document.createElement("img");
	pageL.className = "mangaPage";

	var pageR = document.createElement("img");
	pageR.className = "mangaPage";

	var doubleSpread = document.createElement("img");
	doubleSpread.className = "mangaPage";

	var mangaDisplay = document.createElement("div");
	mangaDisplay.id = "pageDisplay";

	var leftPageContainer = document.createElement("div");
	leftPageContainer.className = "pageContainer";
	leftPageContainer.style.textAlign = "right";
	leftPageContainer.appendChild(pageL);
	mangaDisplay.appendChild(leftPageContainer);

	var rightPageContainer = document.createElement("div");
	rightPageContainer.className = "pageContainer";
	rightPageContainer.appendChild(pageR);
	mangaDisplay.appendChild(rightPageContainer);

	mangaDisplay.appendChild(doubleSpread);

	mangaDiv.appendChild(mangaDisplay);

	var controlBar = document.createElement("div");
	controlBar.id = "controlBar";
	mangaDiv.appendChild(controlBar);

	var leftButton = document.createElement("button");
	leftButton.type = "button";
	leftButton.innerHTML = "\u2b05";
	leftButton.onclick = nextPage;
	controlBar.appendChild(leftButton);

	var fullscreenButton = document.createElement("button");
	fullscreenButton.type = "button";
	fullscreenButton.innerHTML = "⛶";
	fullscreenButton.onclick = function()
	{
		if(document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement)
		{
			if (document.exitFullscreen) document.exitFullscreen();
			else if (document.mozCancelFullScreen) document.mozCancelFullScreen();
			else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
			else if (document.msExitFullscreen) document.msExitFullscreen();
		}
		else
		{
			if(mangaDiv.requestFullscreen) mangaDiv.requestFullscreen();
			else if(mangaDiv.mozRequestFullScreen) mangaDiv.mozRequestFullScreen();
			else if(mangaDiv.webkitRequestFullscreen) mangaDiv.webkitRequestFullscreen();
			else if(mangaDiv.msRequestFullscreen) mangaDiv.msRequestFullscreen();
		}
	};
	controlBar.appendChild(fullscreenButton);

	var rightButton = document.createElement("button");
	rightButton.type = "button";
	rightButton.innerHTML = "\u27a1";
	rightButton.onclick = prevPage;
	controlBar.appendChild(rightButton);

	function nextPage()
	{
		if(currentPage >= 0 && mangaConfig.pages[currentPage].is_spread) turnPage(currentPage + 1);
		else if(currentPage + 2 < mangaConfig.pages.length) turnPage(currentPage + 2);
	}

	function prevPage()
	{
		if(mangaConfig.pages[currentPage - 1].is_spread) turnPage(currentPage - 1);
		else if(currentPage - 1 >= 0) turnPage(currentPage - 2);
	}

	document.onkeydown = function(e)
	{
		if(e.keyCode == '37') nextPage();
		else if(e.keyCode == '39') prevPage();
	}

	getAdsConfig(flashVars.config_url).then(function(config)
	{
		var ads = Array.from(config.getElementsByTagName("manga:preload")[0].getElementsByTagName("adSlots")[0].getElementsByTagName("adSlot")[0].children,child => child.getAttribute("url"));

		if(ads.length > 0)
		{
			var adIndex = 0;
			var currentAd = ads[adIndex];

			var adPlayer = new window.VASTPlayer(playerDiv);

			adPlayer.once('AdStopped', startManga);

			function prepareAd()
			{
				return adPlayer.load(currentAd)
					.catch(function(e)
					{
						if(adIndex < ads.length)
						{
							adIndex++;
							currentAd = ads[adIndex];
							return prepareAd();
						}
					});
			}

			prepareAd().then(readyPlayer => readyPlayer.startAd());
		}
		else startManga();
	});

	function startManga()
	{
		playerDiv.parentNode.replaceChild(mangaDiv,playerDiv);

		authenticate(authToken,flashVars.session_id,flashVars.seriesId).then(function(data)
		{
			if(data) userID = data.user.user_id;
			return getJSON("http://" + flashVars.server + "/list_chapters?series%5Fid=" + flashVars.seriesId + (data ? ("&user%5Fid=" + userID) : ""));
		}).then(function(config)
		{
			var currentChapter = config.chapters.find(chapter => chapter.number == flashVars.chapterNumber);
			var chapterID = currentChapter.chapter_id;
			if(currentChapter.page_logs) currentPage = parseInt(currentChapter.page_logs.current_page) - 1;

			return getJSON("http://" + flashVars.server + "/list_chapter?chapter%5Fid=" + chapterID + "&session%5Fid=" + flashVars.session_id + "&auth=" + authToken);
		}).then(function(config)
		{
			mangaConfig = config;
			turnPage(currentPage);
		});
	}

	function turnPage(newPage)
	{
		var blankPage = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==";
		if(newPage < 0) newPage = 0;
		currentPage = newPage;
		leftPageContainer.style.display = "none";
		rightPageContainer.style.display = "none";
		doubleSpread.style.display = "none";
		setPage(pageL,blankPage);
		setPage(pageR,blankPage);
		setPage(doubleSpread,blankPage);

		var pageData = mangaConfig.pages[newPage];

		var jsonRequest = new XMLHttpRequest();
		jsonRequest.open("POST", "http://" + flashVars.server + "/log_chapterpage", true);
		jsonRequest.responseType = "json";
		jsonRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
		jsonRequest.send("user_id=" + userID + "&api_ver=1.0&page_id=" + pageData.page_id + "&device_type=com.crunchyroll.manga.flash");

		if(pageData.is_spread)
		{
			doubleSpread.style.display = "";
			loadPage(doubleSpread,newPage);
		}
		else
		{
			leftPageContainer.style.display = "";
			rightPageContainer.style.display = "";

			if(pageData.page_number == 0 || pageData.page_number == 1)
			{
				loadPage(pageR,newPage);
				if(mangaConfig.pages[newPage + 1]) loadPage(pageL,newPage + 1);
			}
			else if(pageData.page_number == 2 || pageData.page_number == 6)
			{
				currentPage--;
				loadPage(pageL,newPage);
			}
			else console.warn("Unknown page_number",pageData.page_number);
		}

		function loadPage(element,pageIndex)
		{
			if(pages[newPage]) setPage(element,pages[pageIndex]);
			else getDecryptedImage(mangaConfig.pages[pageIndex].locale[flashVars.locale].encrypted_composed_image_url).then(function(pageURL)
			{
				pages[pageIndex] = pageURL;
				setPage(element,pages[pageIndex]);
			})

			function getDecryptedImage(imageURL)
			{
				return new Promise(function(resolve,reject)
								   {
					var config = new XMLHttpRequest();
					config.onload = function()
					{
						var buffer = this.response;

						var viewer = new DataView(buffer);
						for(var i = 0; i < buffer.byteLength; i++)
						{
							var originalByte = viewer.getUint8(i);
							viewer.setUint8(i,originalByte ^ 0x42);
						}

						resolve(URL.createObjectURL(new Blob([buffer], {type: "image/jpeg"})));
					};
					config.onerror = reject;
					config.open("GET", imageURL, true);
					config.responseType = "arraybuffer";
					config.send();
				});
			}
		}

		function setPage(pageElement,image)
		{
			pageElement.src = image;
		}
	}
}

function authenticate(token,sessionId,seriesId)
{
	return new Promise(function(resolve)
	{
		getJSON("http://api-manga.crunchyroll.com/cr_authenticate?session%5Fid=" + sessionId + "&api%5Fver=1&device%5Ftype=com%2Ecrunchyroll%2Emanga%2Ewww&format=json&series%5Fid=" + seriesId + "&auth=" + token + "&version=0").then(data => resolve(data.data));
	})
}

function parseFlashVars(vars)
{
	var splitVars = vars.split("&");

	var parsed = {};

	for(var s = 0; s < splitVars.length; s++)
	{
		var split = splitVars[s].split("=");

		parsed[split[0]] = decodeURIComponent(split[1]);
	}

	return parsed;
}

function getAdsConfig(configURL)
{
	return new Promise(function(resolve,reject)
	{
		var config = new XMLHttpRequest();
		config.onload = function()
		{
			resolve(this.response);
		};
		config.onerror = reject;
		config.open("GET", configURL, true);
		config.responseType = "document";
		config.send();
	});
}

function getJSON(configURL)
{
	return new Promise(function(resolve,reject)
	{
		var jsonRequest = new XMLHttpRequest();
		jsonRequest.onload = function()
		{
			resolve(this.response);
		};
		jsonRequest.onerror = reject;
		jsonRequest.open("GET", configURL, true);
		jsonRequest.responseType = "json";
		jsonRequest.send();
	});
}