// ==UserScript==
// @name 8Muses Downloader
// @namespace https://github.com/Kayla355
// @version 0.5.1
// @description Download comics from 8muses.com
// @author Kayla355
// @match http://comics.8muses.com/comics/album/*
// @match https://comics.8muses.com/comics/album/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @run-at document-idle
// @icon https://www.8muses.com/favicon.ico
// @require https://cdn.jsdelivr.net/jszip/3.1.3/jszip.min.js
// @require https://cdn.jsdelivr.net/filesaver.js/1.3.3/FileSaver.min.js
// @require https://gitcdn.xyz/repo/Kayla355/MonkeyConfig/d152bb448db130169dbd659b28375ae96e4c482d/monkeyconfig.js
// @history 0.2.0 'Download all', now has an option to compress all the individual .zip files into one zip with sub-folders. By default this will be on.
// @history 0.2.1 Added an option to compress the subfolders created from the single file download option.
// @history 0.2.2 Fixed a bug where it would trigger the download multiple times when the "single file" option was enabled and the "compress sub folders" option was not.
// @history 0.3.0 ALso added the basis for download trying to download something with pagination. However, this is disabled until I solve the issue of running out of memory while doing it.
// @history 0.3.1 Fixed an issue caused by classnames being changed on the site.
// @history 0.3.2 Fixed the URL match since it was changed.
// @history 0.4.0 Updated the script to work with the new use of Ractive.js on the 8muses website.
// @history 0.4.1 Fixed some css to better match the site.
// @history 0.5.0 Fixed Image links and more
// ==/UserScript==
cfg = new MonkeyConfig({
title: '8Muses Downloader - Configuration',
menuCommand: true,
params: {
single_file: {
type: 'checkbox',
default: true
},
compress_sub_folders: {
type: 'checkbox',
default: false
}
},
onSave: setOptions
});
var Settings = {
singleFile: null,
compressSubFolders: null,
};
var zipArray = [];
var containerArray = [];
var downloadType;
var progress = {
pages: {
current: 0,
items: 0,
},
current: 0,
items: 0,
zips: 0
};
function setOptions() {
Settings.singleFile = cfg.get('single_file');
Settings.compressSubFolders = cfg.get('compress_sub_folders');
}
(function() {
'use strict';
setOptions();
init();
})();
function init() {
var imagebox = document.querySelector('.gallery .c-tile:not(.image-a)');
if(imagebox) {
var isImageAlbum = !!imagebox.href.match(/comics\/picture\//i);
if(isImageAlbum) {
createElements('single');
} else {
createElements('multi');
}
} else {
setTimeout(init, 100);
}
}
function createElements(type) {
downloadType = type || 'single';
var downloadText = (downloadType == "multi") ? 'Download All':'Download';
var div = document.createElement('div');
div.className += 'download show-tablet show-desktop block';
div.style = "background-color: #3a4050; border-left: 1px solid #1a1c22;";
var a = document.createElement('a');
a.href = "#";
a.style = "color: #fff; text-decoration: none; padding: 15px 20px 15px 10px;";
a.innerHTML = '<i class="fa fa-arrow-down icon-inline" style="color: #242730;"></i>'+ downloadText;
a.onclick = downloadHandler;
var bar = document.createElement('div');
bar.innerHTML = `<div class="loading-bar" style="position: absolute; right: 0px; top: 50px; background-color: aliceblue; display: none;">
<center class="value" style="position: absolute; left: 0px; right: 0px; color: #242730;">0%</center>
<div class="progressbar" style="width: 0%; height:20px; background-color: #b1c6ff;"></div>
</div>`;
div.append(a);
document.querySelector('#top-menu > div.top-menu-right').append(div);
bar.querySelector('.loading-bar').style.width = document.querySelector('#top-menu > div.top-menu-right .download').clientWidth+'px';
document.querySelector('#content').append(bar);
}
function updateProgressbar(status, hide) {
status = (typeof status === "string") ? status:status+'%';
if(hide) {
document.querySelector('.loading-bar').style.display = 'none';
} else {
document.querySelector('.loading-bar').style.display = '';
document.querySelector('.loading-bar .value').innerText = status;
document.querySelector('.loading-bar .progressbar').style.width = status;
}
}
function downloadHandler(e) {
e.preventDefault();
e.stopPropagation();
if(document.querySelector('.loading-bar').style.display !== "none") return;
if(downloadType == "multi") {
downloadAll();
} else {
downloadComic();
}
}
function downloadComic(container) {
var imageContainers = (container) ? container:document.querySelectorAll('.gallery .c-tile:not(.image-a)');
var images = [];
var doneLength = 0;
var isImageAlbum = !!imageContainers[0].attributes.href.value.match(/comics\/picture\//i);
if(!container) updateProgressbar(0);
if(isImageAlbum) progress.pages.items += imageContainers.length;
if(isImageAlbum) progress.items++;
for(var i=0; i < imageContainers.length; i++) {
images.push({href: location.protocol +'//'+ location.hostname + imageContainers[i].attributes.href.value});
getPageImage(i, images[i], function(j, object) {
images[j].path = object.path;
images[j].name = object.name;
images[j].imageHref = object.imageHref;
images[j].blob = object.blob;
doneLength++;
if(!container) {
updateProgressbar(Math.round((doneLength/imageContainers.length)*100));
} else if(isImageAlbum) {
if(j === 0) progress.current++;
progress.pages.current++;
updateProgressbar(Math.round((progress.pages.current/progress.pages.items)*100));
}
if(doneLength >= imageContainers.length) createZip(images);
});
}
}
function downloadAll(container) {
var itemContainers = (container) ? container:document.querySelectorAll('.gallery .c-tile:not(.image-a)');
var pagination = document.querySelector('.pagination');
// var pagination = false; //Disabled
var items = [];
var doneLength = 0;
var downloadFunc = function(albumContainer) {
//console.log(albumContainer)
var imagebox = albumContainer.querySelectorAll('.gallery .c-tile:not(.image-a)');
var isImageAlbum = !!imagebox[0].attributes.href.value.match(/comics\/picture\//i);
if(isImageAlbum) {
downloadComic(imagebox);
} else {
downloadAll(imagebox);
}
};
if(pagination && !container) {
var lastHref = pagination.querySelector('span:last-child a').attributes.href.value;
var pageCount = parseInt(lastHref.match(/[0-9]+$/)[0]);
var urls = [];
for(let i=1; i <= pageCount; i++) {
urls.push(location.protocol +'//'+ location.hostname + lastHref.replace(/[0-9]+$/, i));
}
getImageAlbum(urls, downloadFunc);
} else {
if(!container) updateProgressbar(0);
for(let i=0; i < itemContainers.length; i++) {
if(!itemContainers[i].attributes.href || itemContainers[i].attributes.href.value == "") continue;
let href = location.protocol +'//'+ location.hostname + itemContainers[i].attributes.href.value;
getImageAlbum(href, downloadFunc);
}
}
}
function getImageAlbum(url, callback) {
if(typeof url === "object" && url.length) {
for(var i=0; i < url.length; i++) {
getImageAlbum(url[i], function(pageContainer) {
var items = pageContainer.querySelector('.gallery').innerHTML;
containerArray.push(items);
if(containerArray.length >= url.length) {
var container = document.implementation.createHTMLDocument().documentElement;
container.innerHTML = '<div class="gallery">' + containerArray.join('') + '</div>';
callback(container);
}
});
}
} else {
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = function(e) {
var container = document.implementation.createHTMLDocument().documentElement;
container.innerHTML = xhr.responseText;
callback(container);
};
xhr.send();
}
}
function getPageImage(i, image, callback) {
var decodePublic = function(t) {
return "!" === (e = t.replace(/>/g, ">").replace(/</g, "<").replace(/&/g, "&")).charAt(0) ? e.substr(1).replace(/[\x21-\x7e]/g, function(t) {
return String.fromCharCode(33 + (t.charCodeAt(0) + 14) % 94);
}) : "";
};
var object = {};
var xhr = new XMLHttpRequest();
xhr.open('GET', image.href);
// xhr.responseType = 'blob';
xhr.onload = function(e) {
var container = document.implementation.createHTMLDocument().documentElement;
container.innerHTML = xhr.responseText;
var data = JSON.parse(decodePublic(container.querySelector("#ractive-public").innerHTML.trim()));
var ext = data.picture.normalizedPath.match(/\..*?$/i);
object.path = image.href.match(/^.*?(picture|album)\/.*?\/(.*\/).*$/i)[2]; // including author
// object.path = image.href.match(/^.*?[0-9]+\/.*?\/(.*\/).*$/)[1]; // no author
//object.name = container.querySelector('.top-menu-breadcrumb li:last-of-type').innerText.trim(); //+ container.querySelector('#imageName').value.match(/\.([0-9a-z]+)(?:[\?#]|$)/i)[0];
//object.name = data.picture.name + ext;
object.name = data.picture.name + ".jpg";
//object.imageHref = 'https://www-8muses-com.cdn.ampproject.org/i/www.8muses.com/image/fl' + container.querySelector('#imageDir').value + container.querySelector('#imageName').value;
//object.imageHref = 'https://www.8muses.com/image/fl/' + data.picture.publicUri + ext;
object.imageHref = 'https://www.8muses.com/image/fl/' + data.picture.publicUri + ".jpg";
console.log(object);
getImageAsBlob(object.imageHref, function(blob) {
if(!blob) return;
object.blob = blob;
callback(i, object);
});
};
xhr.send();
}
function getImageAsBlob(url, callback) {
GM_xmlhttpRequest({
url: url,
method: 'GET',
responseType: 'blob',
onload: function(xhr) {
var blob = xhr.response;
callback(blob);
}
});
// Non-GM CORS xhr request.
// var xhr = new XMLHttpRequest();
// xhr.open('GET', 'https://cors-anywhere.herokuapp.com/'+object.imageHref);
// xhr.responseType = 'blob';
// xhr.onload = function(e) {
// var blob = xhr.response;
// callback(blob);
// }
// xhr.send();
}
function createZip(images) {
var filename = getFileName(images[0].path);
var zip = new JSZip();
// Generate single or multiple zip files.
if(Settings.singleFile && progress.current > 0) {
if(Settings.compressSubFolders) {
for(let i=0; i < images.length; i++) {
zip.file(images[i].name, images[i].blob);
}
generateZip(zip, filename, function(blob, filename) {
zipArray.push({name: filename, blob: blob});
progress.zips++;
if(progress.zips === progress.items) {
var singleZip = new JSZip();
for(let i=0; i < zipArray.length; i++) {
singleZip.file(zipArray[i].name, zipArray[i].blob);
}
generateZip(singleZip, filename.match(/\[(.*)\]/)[1], function(blob, filename) {
saveAs(blob, filename);
});
}
});
} else {
for(let i=0; i < images.length; i++) {
zipArray.push({name: filename +'/'+ images[i].name, blob: images[i].blob});
// zip.file(images[i].name, images[i].blob);
}
if(progress.pages.current === progress.pages.items) {
var singleZip = new JSZip();
for(let i=0; i < zipArray.length; i++) {
singleZip.file(zipArray[i].name, zipArray[i].blob);
}
generateZip(singleZip, filename.match(/\[(.*)\]/)[1], function(blob, filename) {
saveAs(blob, filename);
});
}
}
} else {
for(let i=0; i < images.length; i++) {
zip.file(images[i].name, images[i].blob);
}
generateZip(zip, filename, function(blob, filename) {
saveAs(blob, filename);
});
}
}
// function generateZip(zip, filename, callback) {
// zip.generateAsync({type:"blob"}).then(function (blob) {
// if(progress.pages.current === progress.pages.items) updateProgressbar('Done!');
// if(typeof callback === 'function') callback(blob, filename+'.zip');
// }, function (err) {
// console.error('Error saving zip: ' +err);
// });
// }
function generateZip(zip, filename, callback) {
zip.generateInternalStream({type:"blob", streamFiles: true})
.accumulate(function updateCallback(metadata) {
// console.log(metadata);
updateProgressbar(metadata.percent);
// metadata contains for example currentFile and percent, see the generateInternalStream doc.
}).then(function (blob) {
if(typeof callback === 'function') callback(blob, filename+'.zip');
if(progress.pages.current === progress.pages.items) updateProgressbar('Done!');
// data contains here the complete zip file as a uint8array (the type asked in generateInternalStream)
});
}
function getFileName(pathname) {
var pathArray = pathname.replace(/\/$/, '').split('/');
var filename = "";
for(var i=0; i<pathArray.length; i++) {
let partialName;
if(i === 0) partialName = '['+ pathArray[i] +']';
if(i === 1) partialName = pathArray[i];
if(i >= 2) partialName = ' - '+ pathArray[i];
filename += partialName;
}
return filename;
}