// ==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/dist/vast-player.min.js
// @include http://www.crunchyroll.com/comics*
// @version 0.5.1
// @grant none
// @downloadURL 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\n\
{\n\
background-color: rgb(224, 224, 224);\n\
display: flex;\n\
flex-direction: column;\n\
border: 10px solid rgb(153, 153, 153);\n\
box-sizing: border-box;\n\
}\n\
\n\
#mangaViewer:fullscreen\n\
{\n\
border: 0px;\n\
}\n\
#mangaViewer:-moz-full-screen\n\
{\n\
border: 0px;\n\
}\n\
#mangaViewer:-webkit-full-screen\n\
{\n\
border: 0px;\n\
}\n\
\n\
#pageDisplay\n\
{\n\
width: 100%;\n\
overflow: hidden;\n\
display: flex;\n\
flex: auto;\n\
justify-content: center;\n\
}\n\
\n\
.pageContainer\n\
{\n\
height: 100%;\n\
width: 50%;\n\
}\n\
\n\
.mangaPage\n\
{\n\
width: auto;\n\
height: 100%;\n\
border: 0px;\n\
}\n\
\n\
#controlBar\n\
{\n\
background-color:#999;\n\
height: 100px;\n\
display: flex;\n\
width: 100%;\n\
flex: none;\n\
}\n\
\n\
#mangaViewer:fullscreen #pageDisplay\n\
{\n\
margin-bottom: 10px\n\
}\n\
\n\
#mangaViewer:-moz-full-screen #pageDisplay\n\
{\n\
margin-bottom: 10px\n\
}\n\
\n\
#mangaViewer:-webkit-full-screen #pageDisplay\n\
{\n\
margin-bottom: 10px\n\
}\n\
#mangaViewer:fullscreen #controlBar\n\
{\n\
position: absolute;\n\
bottom: -90px;\n\
transition: 0.5s;\n\
}\n\
\n\
#mangaViewer:-moz-full-screen #controlBar\n\
{\n\
position: absolute;\n\
bottom: -90px;\n\
transition: 0.5s;\n\
}\n\
\n\
#mangaViewer:-webkit-full-screen #controlBar\n\
{\n\
position: absolute;\n\
bottom: -90px;\n\
transition: 0.5s;\n\
}\n\
\n\
#mangaViewer:-moz-full-screen #controlBar:hover\n\
{\n\
bottom: 0;\n\
}\n\
\n\
#controlBar > * {\n\
margin: auto;\n\
}\n\
\n\
#controlBar button {\n\
width: 50px;\n\
height: 50px;\n\
font-size: 26px;\n\
}";
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;
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 = function()
{
if(currentPage >= 0 && mangaConfig.pages[currentPage].is_spread) turnPage(currentPage + 1);
else if(currentPage + 2 < mangaConfig.pages.length) turnPage(currentPage + 2);
};
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 = function()
{
if(mangaConfig.pages[currentPage - 1].is_spread) turnPage(currentPage - 1);
else if(currentPage - 1 >= 0) turnPage(currentPage - 2);
};
controlBar.appendChild(rightButton);
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 = "";
if(pages[newPage]) setPage(doubleSpread,pages[newPage]);
else getDecryptedImage(mangaConfig.pages[newPage].locale[flashVars.locale].encrypted_composed_image_url).then(function(pageURL)
{
pages[newPage] = pageURL;
setPage(doubleSpread,pages[newPage]);
})
}
else
{
leftPageContainer.style.display = "";
rightPageContainer.style.display = "";
if(pageData.page_number == 0 || pageData.page_number == 1)
{
if(pages[newPage]) setPage(pageR,pages[newPage]);
else getDecryptedImage(mangaConfig.pages[newPage].locale[flashVars.locale].encrypted_composed_image_url).then(function(pageURL)
{
pages[newPage] = pageURL;
setPage(pageR,pages[newPage]);
})
if(pages[newPage + 1]) setPage(pageL,pages[newPage + 1]);
else if(newPage + 1 < mangaConfig.pages.length) getDecryptedImage(mangaConfig.pages[newPage + 1].locale[flashVars.locale].encrypted_composed_image_url).then(function(pageURL)
{
pages[newPage + 1] = pageURL;
setPage(pageL,pages[newPage + 1]);
})
}
else if(pageData.page_number == 2 || pageData.page_number == 6)
{
currentPage--;
if(pages[newPage]) setPage(pageL,pages[newPage]);
else getDecryptedImage(mangaConfig.pages[newPage].locale[flashVars.locale].encrypted_composed_image_url).then(function(pageURL)
{
pages[newPage] = pageURL;
setPage(pageL,pages[newPage]);
})
}
}
function setPage(pageElement,image)
{
pageElement.src = image;
//console.log(pageElement,window.getComputedStyle(pageElement).height,parseInt(window.getComputedStyle(pageElement).height));
//if(mangaConfig.pages[newPage].locale[flashVars.locale].composed_image_height) pageElement.style.width = (mangaConfig.pages[newPage].locale[flashVars.locale].composed_image_width/mangaConfig.pages[newPage].locale[flashVars.locale].composed_image_height) * window.getComputedStyle(pageElement).height;
}
}
}
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 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 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();
});
}