// ==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.4.0
// @grant none
// @downloadURL none
// ==/UserScript==
var mangaObject = document.querySelector("object#showmedia_videoplayer_object");
var mangaConfig = {};
var pages = [];
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.style.width = mangaObject.width;
mangaDiv.style.height = mangaObject.height + "px";
mangaDiv.style.position = "relative";
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.width = "460";
pageL.height = "690";
var pageR = document.createElement("img");
pageR.width = "460";
pageR.height = "690";
var doubleSpread = document.createElement("img");
doubleSpread.width = "920";
doubleSpread.height = "690";
doubleSpread.style.display = "none";
mangaDiv.appendChild(pageL);
mangaDiv.appendChild(pageR);
mangaDiv.appendChild(doubleSpread);
var leftButton = document.createElement("div");
leftButton.style.position = "absolute";
leftButton.style.top = "50%";
leftButton.style.cursor = "pointer";
leftButton.style.color = "white";
leftButton.style.backgroundColor = "black";
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);
};
mangaDiv.appendChild(leftButton);
var rightButton = document.createElement("div");
rightButton.style.position = "absolute";
rightButton.style.top = "50%";
rightButton.style.right = "0";
rightButton.style.cursor = "pointer";
rightButton.style.color = "white";
rightButton.style.backgroundColor = "black";
rightButton.innerHTML = "\u27a1";
rightButton.onclick = function()
{
if(mangaConfig.pages[currentPage - 1].is_spread) turnPage(currentPage - 1);
else if(currentPage - 1 >= 0) turnPage(currentPage - 2);
};
mangaDiv.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 currentAd = ads[ads.length - 1];
var adPlayer = new window.VASTPlayer(playerDiv);
adPlayer.once('AdStopped', startManga);
adPlayer.load(currentAd).then(readyPlayer => readyPlayer.startAd());
}
});
function startManga()
{
playerDiv.parentNode.replaceChild(mangaDiv,playerDiv);
authenticate(authToken,flashVars.session_id,flashVars.seriesId).then(function(data)
{
return getJSON("http://" + flashVars.server + "/list_chapters?series%5Fid=" + flashVars.seriesId + (data ? ("&user%5Fid=" + data.user.user_id) : ""));
}).then(function(config)
{
var chapterID = config.chapters.find(chapter => chapter.number == flashVars.chapterNumber).chapter_id;
return getJSON("http://" + flashVars.server + "/list_chapter?chapter%5Fid=" + chapterID + "&session%5Fid=" + flashVars.session_id + "&auth=" + authToken);
}).then(function(config)
{
mangaConfig = config;
turnPage(0);
});
}
function turnPage(newPage)
{
if(newPage < 0) newPage = 0;
currentPage = newPage;
pageL.style.display = "none";
pageR.style.display = "none";
doubleSpread.style.display = "none";
setPage(pageL,"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==");
setPage(pageR,"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==");
setPage(doubleSpread,"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==");
var pageData = mangaConfig.pages[newPage];
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
{
pageL.style.display = "";
pageR.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;
}
}
}
function authenticate(token,sessionId,seriesId)
{
return new Promise(function(resolve)
{
if(!token) resolve(null);
else 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();
});
}