// ==UserScript==
// @name Let's panda!
// @namespace https://github.com/Sean2525/Let-s-panda
// @author sean2525, strong-Ting
// @description A login, view, download tool for exhentai
// @license MIT
// @require https://code.jquery.com/jquery-3.2.1.slim.min.js
// @include https://exhentai.org/
// @include https://exhentai.org/g/*
// @include https://e-hentai.org/g/*
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.4/jszip.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.3/FileSaver.min.js
// @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
// @grant GM_xmlhttpRequest
// @grant GM.xmlHttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM.setValue
// @grant GM.getValue
// @connect *
// @run-at document-end
// @version 0.1.7
// ==/UserScript==
jQuery(function($) {
/**
* Output extension
* @type {String} zip
* cbz
*
* Tips: Convert .zip to .cbz
* Windows
* $ ren *.zip *.cbz
* Linux
* $ rename 's/\.zip$/\.cbz/' *.zip
*/
var outputExt = 'cbz'; // or 'zip'
/**
* Multithreading
* @type {Number} [1 -> 32]
*/
var threading = 8;
/**
* Logging
* @type {Boolean}
*/
var debug = false;
const loginPage = () => {
let div = document.createElement('div');
div.className = "main";
let username = document.createElement('input');
let style = document.createElement('style');
style.innerHTML = `
.main {
display: -webkit-flex;
display: flex;
-webkit-flex-direction: column;
flex-direction: column;
-webkit-align-items: center;
align-items: center;
-webkit-justify-content: center;
justify-content: center;
height: ${window.innerHeight}px;
}
.flex-center{
display: -webkit-flex;
display: flex;
-webkit-align-items: center;
align-items: center;
-webkit-justify-content: center;
justify-content: center;
}
form {
display: -webkit-flex;
display: flex;
-webkit-flex-direction: column;
flex-direction: column;
-webkit-align-items: center;
align-items: center;
-webkit-justify-content: center;
justify-content: center;
}
.image {
position: relative;
margin: 0;
}
.input {
margin-top: 10px;
display: block;
height: 34px;
padding: 6px 12px;
font-size: 14px;
line-height: 1.42857143;
color: #555;
background-color: #fff;
background-image: none;
border: 1px solid #ccc;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
-webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
-o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
}
.btn {
color: #fff;
background-color: #5cb85c;
border-color: #4cae4c;
margin-top: 10px;
display: inline-block;
font-weight: 400;
line-height: 1.25;
text-align: center;
white-space: nowrap;
vertical-align: middle;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
border: 1px solid transparent;
padding: .5rem 1rem;
font-size: 1rem;
border-radius: .25rem;
-webkit-transition: all .2s ease-in-out;
-o-transition: all .2s ease-in-out;
transition: all .2s ease-in-out;
}
`;
$('head').append(style);
const setCookie = (headers) => {
// only tampermonkey can get cookies from GM_xmlhttpRequest
try{
headers.split('\r\n').find(x => x.match('cookie')).replace('set-cookie: ', "").split('\n').map( x => document.cookie = x.replace('.e-hentai.org', '.exhentai.org'));
}catch(err){
if(debug) console.log(err);
}
document.cookie = "yay=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=.exhentai.org; path=/;";
window.location.reload();
};
const clearCookie = () => {
document.cookie = "yay=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=.exhentai.org; path=/;";
window.location.reload();
};
let form = document.createElement('form');
let login = document.createElement('button');
let wrapper = document.createElement('div');
let loadding = document.createElement('img');
let password = document.createElement('input');
let error = document.createElement('p');
error.innerHTML = `
if you can not login , go to <a target="_blank" href="https://forums.e-hentai.org/index.php?act=Login&CODE=00" style="color:red">here</a> login <br >
make sure login success, then click <button class="clearCookie">here</button>
`;
error.style.color = "white";
$('img')[0].className = "image";
username.type = "text";
username.className = "input";
password.type = "password";
password.className = "input";
loadding.src = "data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA=="
loadding.style.position = "relative";
loadding.hidden = true;
login.addEventListener('click', () => {
loadding.hidden = false;
GM.xmlHttpRequest({
method: "POST",
url: "https://forums.e-hentai.org/index.php?act=Login&CODE=01",
data: `referer=https://forums.e-hentai.org/index.php?&b=&bt=&UserName=${username.value}&PassWord=${password.value}&CookieDate=1"}`,
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
onload: function(response) {
if(debug) console.log(response);
if(/You are now logged/.exec(response.responseText)){
setCookie(response.responseHeaders);
}
loadding.hidden = true;
},
onerror: function(err) {
if(debug) console.log(err);
loadding.hidden = true;
}
});
});
login.className = "btn";
login.innerHTML = "Login";
form.append(username);
form.append(password);
wrapper.className = "flex-center";
wrapper.append(loadding);
wrapper.append(login);
form.append(wrapper);
form.addEventListener('submit', e => {
e.preventDefault();
});
div.append($('img')[0]);
div.append(form);
div.append(error);
$('body').append(div);
$('.clearCookie').on('click', clearCookie);
};
const downloadPage = () => {
var zip = new JSZip(),
doc = document,
tit = doc.title,
$win = $(window),
loc = /.*\//.exec(doc.location.href)[0],
prevZip = false,
current = 0,
images = [],
total = 0,
final = 0,
hrefs = [],
comicId = location.pathname.match(/\d+/)[0],
download = document.createElement('p');
const dlImg = ({index, url}, success, error) => {
var filename = url.replace(/.*\//g, '');
var extension = filename.split('.').pop();
filename = ('0000' + index).slice(-4) + '.' + extension;
if (debug) console.log(filename, 'progress');
GM.xmlHttpRequest({
method: 'GET',
url: url,
responseType: 'arraybuffer',
onload: function (response) {
final++;
success(response, filename);
},
onerror: function (err) {
final++;
error(err, filename);
}
});
};
const next = () => {
download.innerHTML = `<img src="https://exhentai.org/img/mr.gif"> <a href="#"> Downloading ${final}/${total}</a>`;
if (debug) console.log(final, current);
if (final < current) return;
(final < total) ? addZip() : genZip();
};
const end = () => {
$win.off('beforeunload');
if (debug) console.timeEnd('eHentai');
}
const genZip = () => {
zip.generateAsync({
type: 'blob'
}).then(function (blob) {
var zipName = tit
.replace(/\s/g, '_') + '.' + comicId + '.' + outputExt;
if (prevZip) window.URL.revokeObjectURL(prevZip);
prevZip = blob;
saveAs(blob, zipName);
if (debug) console.log('COMPLETE');
download.innerHTML = `<img src="https://exhentai.org/img/mr.gif"> <a href="${window.URL.createObjectURL(prevZip)}" download="${zipName}"> Download completed!</a>`;
end();
});
};
const addZip = () => {
total = images.length;
var max = current + threading;
if(max > total) max = total;
for(current; current < max ; current++){
dlImg(images[current], function (response, filename) {
zip.file(filename, response.response);
if (debug) console.log(filename, 'success');
next();
}, function (err, filename) {
zip.file(filename + '_' + comicId + '_error.gif', 'R0lGODdhBQAFAIACAAAAAP/eACwAAAAABQAFAAACCIwPkWerClIBADs=', {
base64: true
});
if (debug) console.log(filename, 'error');
next();
});
}
};
const getImageNext = () => {
download.innerHTML = `<img src="https://exhentai.org/img/mr.gif"> <a href="#">Getting images ${final}/${hrefs.length}</a>`;
if (debug) console.log(final, current);
if (final < current) return;
(final < hrefs.length) ? getImage() :(() => {
current = 0;
final = 0;
addZip();
})();
};
const getImage = () => {
let max = current + threading;
if(max > hrefs.length) max = hrefs.length;
for(current; current < max ; current++){
if(debug) console.log(hrefs[current]);
GM.xmlHttpRequest({
method: "GET",
url: hrefs[current],
onload: function(response) {
let imgNo = parseInt(response.responseText.match("startpage=(\\d+)").pop());
let img = (new DOMParser()).parseFromString(response.responseText, "text/html").querySelector("#img");
if (debug) console.log(imgNo, 'success');
images.push({ index: imgNo ,url:img.src});
final++;
getImageNext();
},
onerror: function(err) {
final++;
getImageNext();
if(debug) console.log(err);
}
});
}
};
const getHref = () => {
let page = document.querySelector('table[class=ptt] tbody tr').childNodes.length-2;
for(let i = 0 ; i < page; i++){
GM.xmlHttpRequest({
method: "GET",
url: `${loc}?p=${i}`,
onload: function(response) {
let imgs = [...(new DOMParser()).parseFromString(response.responseText, "text/html").querySelectorAll(".gdtm a")];
imgs.forEach((v) => {
hrefs.push(v.href);
});
if(i == page -1){
getImage();
}
},
onerror: function(err) {
download.innerHTML = '<img src="https://exhentai.org/img/mr.gif"> <a href="#">Get href failed</a>';
if(i == page -1){
getImage();
}
if(debug) console.log(err);
}
});
}
};
download.className = "g3 gsp";
download.innerHTML = `<img src="https://exhentai.org/img/mr.gif"> <a class="panda_download" href="#">Download</a>`;
$('#gd5').append(download);
$('.panda_download').on('click', () => {
if(threading < 1) threading = 1;
if(threading > 32) threading = 32;
if (debug) console.time('eHentai');
$win.on('beforeunload', function () {
return 'Progress is running...';
});
download.innerHTML = `<img src="https://exhentai.org/img/mr.gif"> <a href="#">Start Download</a>`;
getHref();
});
};
function view(){
var gdt = document.querySelector('#gdt');
var gdd = document.querySelector('#gdd');
var gdo4 = document.querySelector('#gdo4');
var lpPage = (document.querySelectorAll("table.ptt td").length - 2);
var data = document.querySelector("body div.gtb p.gpc").textContent.split(" ");
var minPic = parseInt(data[1]);
var maxPic = parseInt(data[3]);
var imgNum = parseInt(gdd.querySelector("#gdd tr:nth-child(n+6) td.gdt2").textContent.split(" ")[0]);
var pagePic = maxPic - minPic +1;
var status = "false";
viewer(document.querySelectorAll("table.ptt td").length - 2,imgNum ,minPic,maxPic);
async function viewer(lpPage, imgNum,minPic,maxPic) {
var Gallery = function(pageNum, imgNum,minPic,maxPic) {
this.pageNum = pageNum || 0;
this.imgNum = imgNum || 0;
};
Gallery.prototype = {
imgList: [],
checkFunctional: function() {
return (this.imgNum > 41 && this.pageNum < 2) || this.imgNum !== 0;
},
loadPageUrls: function(element) {
[].forEach.call(element.querySelectorAll("a[href]"), function(item) {
console.log('load work');
var ajax = new XMLHttpRequest();
ajax.onreadystatechange = async function() {
if (4 == ajax.readyState && 200 == ajax.status) {
var imgNo = parseInt(ajax.responseText.match("startpage=(\\d+)").pop());
var src = (new DOMParser()).parseFromString(ajax.responseText, "text/html").getElementById("img").src;
Gallery.prototype.imgList[imgNo-1].src = src;
if(await GM.getValue("width") == undefined){
GM.setValue('width','0.7');
console.log('set width:0.7');
}
if(await GM.getValue("mode") == undefined){
GM.setValue('mode','single');
console.log('set mode:single');
}
$('#gdt').find('img').css('width',$(window).width()*(await GM.getValue("width")));
}
};
ajax.open("GET", item.href);
ajax.send(null);
});
},
getNextPage: function() {
var LoadPageUrls = this.loadPageUrls;
var download = this.download_file;
for (var i = 0; i < this.pageNum; ++i) {
var ajax = new XMLHttpRequest();
ajax.onreadystatechange = function() {
if (4 == this.readyState && 200 == this.status) {
var dom = (new DOMParser()).parseFromString(this.responseText, "text/html");
LoadPageUrls(dom.getElementById("gdt"));
}
};
ajax.open("GET", location.href + "?p=" + i);
ajax.send(null);
}
},
claenGDT: function() {
while (gdt.firstChild && gdt.firstChild.className)
gdt.removeChild(gdt.firstChild);
},
generateImg: function(callback) {
for (var i = 0; i < this.imgNum; i++) {
if(i<maxPic && i >= minPic - 1 ){
var img = document.createElement('img');
img.setAttribute("src", "http://ehgt.org/g/roller.gif");
this.imgList.push(img);
gdt.appendChild(img);
}
else{
var img = document.createElement("img");
this.imgList.push(img);
// gdt.appendChild(img); //cant load all
}
}
gdt.style.textAlign="center";
gdt.style.maxWidth="100%";
gdo4.innerHTML= ""; //clear origin button(Normal Large)
var style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = `
div#gdo4{
position:absolute;
width: 135px;
height:32px;
left:-50px;
top:-40px;
text-align:right;
z-index:1;
}
.double {
font-weight: bold;
// margin: 0 2px 4px 2px;
float: left;
border-radius: 5px;
height:32px;
width: 32px;
//border: 1px solid #989898;
//background: #4f535b;
background-image: url(https://raw.githubusercontent.com/Sean2525/Let-s-panda/master/icons/2_32.png);
}
.double:hover{
background: #4f535b;
background-image: url(https://raw.githubusercontent.com/Sean2525/Let-s-panda/master/icons/2_32.png);
}
.single{
font-weight: bold;
// margin: 0 2px 4px 2px;
float: left;
border-radius: 5px;
height:32px;
width: 32px;
//border: 1px solid #989898;
// background: #4f535b;
background-image: url(https://raw.githubusercontent.com/Sean2525/Let-s-panda/master/icons/1_32.png);
}
.size_pic{
font-weight: bold;
// margin: 0 2px 4px 2px;
float: left;
border-radius: 2px;
height:16px;
width: 16px;
//border: 1px solid #989898;
// background: #4f535b;
}
.single:hover{
background: #4f535b;
background-image: url(https://raw.githubusercontent.com/Sean2525/Let-s-panda/master/icons/1_32.png);
}
.size_btn {
height: 32px;
width: 32px;
border-radius: 100%;
//font-family: Arial;
color: #ffffff;
font-size: 16px;
background: #4f535b;
text-decoration: none;
}
.size_btn:hover {
background: #a9adb1;
text-decoration: none;
}
`;
document.getElementsByTagName('head')[0].appendChild(style);
//show
var single_pic = document.createElement("div");//create single button
single_pic.className = "single";
single_pic.innerHTML += '';
gdo4.appendChild(single_pic);
var double_pic = document.createElement("div"); //create double button
double_pic.className = "double";
double_pic.innerHTML = '';
gdo4.appendChild(double_pic);
var size_pic_add = document.createElement("button");
size_pic_add.className = "size_btn";
size_pic_add.innerHTML += '+';
gdo4.appendChild(size_pic_add);
var size_pic_reduce = document.createElement("button");
size_pic_reduce.className = "size_btn";
size_pic_reduce.innerHTML += '-';
gdo4.appendChild(size_pic_reduce);
/*
const wrap =(width)=>{
let img = $('#gdt').find('img');
for(let i = 0;i<img.length;i++){
let wrap = document.createElement('wrap');
wrap.innerHTML='<br>';
if(width>0.5){
gdt.insertBefore(wrap,img[i]);
}
else if(width<=0.5){
if(i%2!==1){
gdt.insertBefore(wrap,img[i]);
}
}
}
}
*/
document.getElementById('gdo4').children[0] //when single button click change value of width
.addEventListener('click', async function (event) {
GM.setValue("width", "0.7");
GM.setValue("mode",'single');
pic_width(await GM.getValue("width"));
$('wrap').remove();
wrap(await GM.getValue("width"));
});
document.getElementById('gdo4').children[1] //when double button click change value of width
.addEventListener('click', async function (event) {
GM.setValue("width", "0.48");
GM.setValue("mode",'double');
pic_width(await GM.getValue("width"));
$('wrap').remove();
wrap(await GM.getValue("mode"));
});
document.getElementById('gdo4').children[2]
.addEventListener('click', async function (event) {
var size_width = parseFloat(await GM.getValue("width"));
if(size_width>0.2 && size_width<0.9){
size_width = size_width + 0.1;
GM.setValue("width",size_width);
}
let _width = await GM.getValue('width');
pic_width(_width);
console.log(_width);
});
document.getElementById('gdo4').children[3]
.addEventListener('click', async function (event) {
var size_width = parseFloat(await GM.getValue("width"));
if(size_width>0.3 && size_width<1){
size_width = size_width - 0.1;
GM.setValue("width",size_width);
}
let _width = await GM.getValue('width');
pic_width(_width);
console.log(_width);
});
function pic_width(width)//change width of pics
{
for(var i = (maxPic-minPic+1);i>0;i--)
{
$('#gdt').find('img').css('width',$(window).width()*width);
}
}
callback && callback();
}
};
var g = new Gallery(lpPage,imgNum,minPic,maxPic);
if (g.checkFunctional()) {
g.generateImg(function() {
g.loadPageUrls(gdt);
g.claenGDT();
// if (g.pageNum)
// g.getNextPage('load');
});
wrap(await GM.getValue("mode"));
}
else {
alert("There are some issue in the script\nplease open an issue on Github");
//window.open("https://github.com/strong-Ting/Gentle-Viewer/issues");
}
function download_files(lpPage ,imgNum,minPic,maxPic)
{
console.log(lpPage,imgNum,minPic,maxPic);
var download_obj = new Gallery(lpPage,imgNum,minPic,maxPic);
download_obj.download();
}
}
}
const wrap = async width =>{
let img = $('#gdt').find('img');
let gdt = document.getElementById('gdt');
for(let i = 0;i<img.length;i++){
let wrap = document.createElement('wrap');
wrap.innerHTML='<br>';
if(await GM.getValue("mode") == 'single'){
gdt.insertBefore(wrap,img[i]);
}
else if(await GM.getValue("mode")=='double'){
if(i%2!==1){
gdt.insertBefore(wrap,img[i]);
}
}
}
}
if((e = $('img')).length === 1 && e[0].src === window.location.href){
loginPage();
} else if (window.location.href.match(/^https:\/\/e[x-]hentai\.org\/g/)){
downloadPage();
view();
}
});