// ==UserScript==
// @name Crunchyroll HTML5 Manga Viewer
// @namespace DoomTay
// @description Replaces Flash manga viewer with an HTML5 setup
// @require https://cdn.jsdelivr.net/npm/vast-player@0.2.10/dist/vast-player.min.js
// @include http://www.crunchyroll.com/comics*
// @include https://www.crunchyroll.com/comics*
// @version 0.6.1
// @grant none
// @downloadURL https://update.greasyfork.icu/scripts/40571/Crunchyroll%20HTML5%20Manga%20Viewer.user.js
// @updateURL https://update.greasyfork.icu/scripts/40571/Crunchyroll%20HTML5%20Manga%20Viewer.meta.js
// ==/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.replace("http:","https:")).then(function(config)
{
var ads = Array.from(config.getElementsByTagName("manga:preload")[0].getElementsByTagName("adSlots")[0].getElementsByTagName("adSlot")[0].children,child => child.getAttribute("url").replace("http:","https:"));
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("https://" + 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("https://" + 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", "https://" + 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("https://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();
});
}