// ==UserScript==
// @name NovelCrow Comic Downloader
// @version 2.3
// @description Download comics from novelcrow.com with forced image loading, custom zip naming based on URL, and correct image retrieval
// @author B14ckwxd
// @match *://novelcrow.com/comic/*
// @require https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.2.1.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.2.2/jszip.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.min.js
// @grant GM_xmlhttpRequest
// @noframes
// @run-at document-idle
// @namespace https://greasyfork.org/users/1462126
// ==/UserScript==
(function() {
'use strict';
// Ensure jQuery is loaded
if (typeof jQuery === 'undefined') {
console.error("jQuery is not loaded. The script will not run.");
return;
}
console.log("📥 NovelCrow Comic Downloader script is running...");
// Wait for page to load dynamically
const observer = new MutationObserver(() => {
if ($('.page-break img').length) {
console.log("📸 Comic images detected. Ready to download.");
observer.disconnect();
addDownloadButton();
}
});
observer.observe(document.body, { childList: true, subtree: true });
function addDownloadButton() {
if ($('#downloadBtn').length) return; // Prevent duplicate buttons
console.log("🛠 Adding download button...");
var downBtn = $('<button/>', {
id: 'downloadBtn',
text: 'DOWNLOAD CHAPTER',
css: {
position: 'fixed',
bottom: '20px',
right: '20px',
backgroundColor: '#2a518e',
color: '#ffffff',
fontWeight: 'bold',
padding: '10px 20px',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
zIndex: '10000',
boxShadow: '0 2px 5px rgba(0,0,0,0.2)'
}
});
// Append button to the page
$('body').append(downBtn);
// Button click handler
downBtn.click(startDownload);
}
// Force load all images function
function forceLoadAllImages() {
console.log("🔄 Forcing all images to load...");
return new Promise(resolve => {
let imgs = $('.page-break img');
let promises = [];
imgs.each(function() {
let $img = $(this);
// If the image is lazy-loaded, force its src to the full image URL
let dataSrc = $img.attr('data-src');
if (dataSrc) {
let fullSrc = dataSrc.replace(/\/thumbs?\//, '/full/');
if ($img.attr('src') !== fullSrc) {
$img.attr('src', fullSrc);
}
}
// Create a promise that resolves when the image is loaded (or errors)
let p = new Promise(r => {
if (this.complete) {
r();
} else {
$img.on('load error', r);
}
});
promises.push(p);
});
Promise.all(promises).then(() => {
console.log("✅ All images have been forced to load.");
resolve();
});
});
}
// Mark startDownload as async so we can use await
async function startDownload() {
var downBtn = $('#downloadBtn');
var downloading = false;
var downloaded = false;
var images = [];
var zip = new JSZip();
var title = 'comic_chapter';
// Custom title extraction based on URL
// Expected URL structure: /comic/{comic-slug}/{chapter-slug}/
var pathParts = window.location.pathname.split('/').filter(Boolean);
if (pathParts.length >= 3 && pathParts[0].toLowerCase() === 'comic') {
let chapterSlug = pathParts[2]; // e.g., "4-the-ortegas-chronicles-chapter-4"
let match = chapterSlug.match(/^(\d+)-(.+)-chapter-\d+$/i);
if (match) {
let chapterNum = match[1]; // "4"
let titlePart = match[2]; // "the-ortegas-chronicles"
let formattedTitle = titlePart.replace(/-/g, ' ').trim(); // "the ortegas chronicles"
chapterNum = ('0' + chapterNum).slice(-2); // format as "04"
title = formattedTitle + ' - Issue ' + chapterNum;
} else {
// Fallback: try to use the chapter title from the page
try {
var pageTitle = $('.wp-manga-chaptertitle').text().trim();
if (pageTitle) {
title = pageTitle.replace(/[^a-zA-Z0-9]/g, '_').substring(0, 50);
} else {
throw "no title";
}
} catch (e) {
console.warn("⚠ Could not retrieve chapter title. Deriving title from URL.");
title = window.location.pathname.replace(/[^a-zA-Z0-9]/g, '_').substring(0, 50);
}
}
}
// If already downloading or downloaded, generate the zip directly
if (downloading || downloaded) {
zip.generateAsync({type: 'blob'}).then(function(blob) {
saveAs(blob, title + '.zip');
});
return;
}
// Initialize download
downloading = true;
downBtn.text('LOADING IMAGES...').css('background-color', '#dbba00');
// Force all images to load
await forceLoadAllImages();
// Find all comic images
$('.page-break img').each(function() {
var imgSrc = $(this).attr('data-src') || $(this).attr('src');
if (imgSrc) {
images.push(imgSrc.replace(/\/thumbs?\//, '/full/')); // Convert to full-size URL
}
});
if (images.length === 0) {
console.error("❌ No images found!");
downBtn.text('NO IMAGES FOUND').css('background-color', '#d9534f');
return;
}
console.log(`🔄 Found ${images.length} images. Starting download...`);
downBtn.text(`0/${images.length} DOWNLOADED`);
var downCount = 0;
var pad = '0000';
var incomplete = false;
images.forEach(function(url, i) {
GM_xmlhttpRequest({
method: 'GET',
url: url,
responseType: 'arraybuffer',
headers: {
Referer: window.location.href
},
onload: function(response) {
// Determine file extension based on URL, default to .jpg
var extMatch = url.match(/\.(webp|jpg|jpeg|png)(\?|$)/i);
var ext = extMatch && extMatch[1] ? '.' + extMatch[1].toLowerCase() : '.jpg';
var fileName = pad.substr(0, pad.length - (i + 1).toString().length) + (i + 1) + ext;
zip.file(fileName, response.response);
updateProgress();
},
onerror: function() { handleError(url); },
onabort: function() { handleError(url); },
ontimeout: function() { handleError(url); }
});
function updateProgress() {
downCount++;
downBtn.text(`${downCount}/${images.length} DOWNLOADED`);
if (downCount === images.length) finalizeDownload();
}
function handleError(url) {
console.error('❌ Failed to download image:', url);
incomplete = true;
downCount++;
updateProgress();
}
});
function finalizeDownload() {
zip.generateAsync({type: 'blob'}).then(function(blob) {
// Save the zip file with the custom title
saveAs(blob, title + '.zip');
downBtn.text('DOWNLOAD COMPLETE').css('background-color', '#216d28');
downloaded = true;
downloading = false;
console.log("✅ Download completed!");
});
}
}
})();