// ==UserScript==
// @name TruyenFull downloader
// @name:vi TruyenFull downloader
// @namespace https://baivong.github.io/
// @description Tải truyện từ TruyenFull định dạng EPUB.
// @description:vi Tải truyện từ TruyenFull định dạng EPUB.
// @version 4.6.1
// @icon https://i.imgur.com/FQY8btq.png
// @author Zzbaivong
// @oujs:author baivong
// @license MIT; https://baivong.mit-license.org/license.txt
// @match https://truyenfull.vn/*
// @match https://truyenfull.vision/*
// @require https://code.jquery.com/jquery-3.7.1.min.js
// @require https://unpkg.com/[email protected]/dist/jszip.min.js
// @require https://unpkg.com/[email protected]/dist/FileSaver.min.js
// @require https://unpkg.com/[email protected]/ejs.min.js
// @require https://unpkg.com/[email protected]/dist/jepub.js
// @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js?v=a834d46
// @noframes
// @connect *
// @supportURL https://github.com/lelinhtinh/Userscript/issues
// @run-at document-idle
// @grant GM_xmlhttpRequest
// @grant GM.xmlHttpRequest
// ==/UserScript==
(function ($, window, document) {
'use strict';
// ===== SETTINGS =====
const settings = {
errorAlert: false,
allowedImageExtensions: ['jpg', 'jpeg', 'png', 'webp'],
};
// ===== UTILITY FUNCTIONS =====
const chunkArray = (arr, per) => {
return arr.reduce((resultArray, item, index) => {
const chunkIndex = Math.floor(index / per);
if (!resultArray[chunkIndex]) resultArray[chunkIndex] = [];
resultArray[chunkIndex].push(item);
return resultArray;
}, []);
};
const cleanHtml = (str) => {
str = str.replace(/\s*Chương\s*\d+\s?:[^<\n]/, '');
str = str.replace(/[^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]+/gm, ''); // eslint-disable-line
return '<div>' + str + '</div>';
};
const beforeleaving = (e) => {
e.preventDefault();
e.returnValue = '';
};
const shouldSkipImage = (imageUrl) => {
const urlExtension = imageUrl.match(/\.(\w+)(\?|#|$)/i);
const extension = urlExtension ? urlExtension[1].toLowerCase() : null;
// Skip images with unsupported extensions (but allow images without extension)
return extension && !settings.allowedImageExtensions.includes(extension);
};
const processSingleImage = async (imgElement, imgSrc, imageIndex, totalImages) => {
if (!imgSrc) {
imgElement.remove();
return;
}
try {
const absoluteUrl = new URL(imgSrc, locationInfo.referrer).href;
if (shouldSkipImage(absoluteUrl)) {
console.log(`Bỏ qua ảnh có định dạng không hỗ trợ: ${absoluteUrl}`);
imgElement.remove();
return;
}
console.log(`Đang tải ảnh ${imageIndex + 1}/${totalImages}: ${absoluteUrl}`);
const imageId = await downloadAndAddImage(absoluteUrl, `chap_${chapterState.current.id}_img_${imageIndex}`);
imgElement.replaceWith(`<p><img src="${imageId}" alt="Hình ảnh chương ${chapterState.current.id}" /></p>`); // Delay between images to avoid rate limiting
if (imageIndex < totalImages - 1) {
await new Promise((resolve) => setTimeout(resolve, 500));
}
} catch (error) {
console.warn('Không thể tải ảnh:', imgSrc, error);
imgElement.replaceWith('<br /><a href="' + imgSrc + '">Click để xem ảnh</a><br />');
}
};
const processChapterImages = async ($chapter) => {
const $images = $chapter.find('img');
if (!$images.length) return;
// Process each image sequentially
for (let i = 0; i < $images.length; i++) {
await processSingleImage($images.eq(i), $images.eq(i).attr('src'), i, $images.length);
}
};
const cleanChapterContent = ($chapter) => {
// Remove unwanted elements
const $unwantedElements = $chapter.find('script, style, a');
const $hiddenElements = $chapter.find('[style]').filter(function () {
return this.style.fontSize === '1px' || this.style.fontSize === '0px' || this.style.color === 'white';
});
$unwantedElements.remove();
$hiddenElements.remove();
return $chapter.text().trim() !== '' ? cleanHtml($chapter.html()) : null;
};
const extractChapterTitle = ($data) => {
let title = $data.find('.chapter-title').text().trim();
if (!title) {
const chapterMatch = chapterState.current.id.match(/\d+/);
title = chapterMatch ? `Chương ${chapterMatch[0]}` : 'Chương không xác định';
}
return title;
};
const updateDownloadProgress = () => {
const progressText = `Đang tải <strong>${chapterState.current.index}/${chapterState.size}${
partState.size ? '/' + (partState.current + 1) : ''
}</strong>`;
ui.$download.html(progressText);
document.title = `[${chapterState.current.index}] ${ui.pageName}`;
console.log(`Đã tải: ${chapterState.current.index}/${chapterState.size} - ${chapterState.current.title}`);
};
const downloadAndAddImage = async (imgUrl, imageId) => {
return new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'GET',
url: imgUrl,
responseType: 'arraybuffer',
timeout: 15000, // 15 second timeout
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
Referer: locationInfo.referrer,
},
onload: (response) => {
try {
if (response.status === 200 && response.response && response.response.byteLength > 0) {
libs.jepub.image(response.response, imageId);
console.log(
`Đã tải thành công ảnh: ${imageId} (${Math.round(response.response.byteLength / 1024)}KB)`,
);
resolve(imageId);
} else {
reject(new Error(`HTTP ${response.status} hoặc ảnh rỗng`));
}
} catch (error) {
reject(error);
}
},
onerror: (error) => {
reject(new Error('Lỗi mạng khi tải ảnh'));
},
ontimeout: () => {
reject(new Error('Timeout khi tải ảnh'));
},
});
});
};
// ===== GLOBAL STATE OBJECTS =====
// URL and Location Information
const locationInfo = {
host: location.host,
pathname: location.pathname,
referrer: location.protocol + '//' + location.host + location.pathname,
novelAlias: location.pathname.slice(1, -1),
};
// Ebook Metadata
const ebookInfo = {
title: $('h1').text().trim(),
author: $('.info a[itemprop="author"]').text().trim(),
cover: $('.books img').attr('src'),
description: $('.desc-text').html(),
genres: [],
credits: `<p>Truyện được tải từ <a href="${locationInfo.referrer}">TruyenFull</a></p><p>Userscript được viết bởi: <a href="https://lelinhtinh.github.io/jEpub/">lelinhtinh</a></p>`,
};
// Chapter Management
const chapterState = {
list: [],
size: 0,
current: {
id: '',
title: '',
index: 0,
},
progress: {
begin: '',
end: '',
summary: '',
},
};
// Part Management (for splitting large books)
const partState = {
list: [],
size: 0,
current: 0,
};
// Download State
const downloadState = {
status: '',
isFinished: false,
hasErrors: false,
delay: 0,
errorTitles: [],
};
// UI Elements
const ui = {
pageName: document.title,
$download: $('<a>', {
class: 'btn btn-primary',
href: '#download',
text: 'Tải xuống',
}),
$novelId: $('#truyen-id'),
};
// External Libraries
const libs = {
jepub: null,
};
// Helper function for download status
const downloadStatus = (label) => {
downloadState.status = label;
ui.$download.removeClass('btn-primary btn-success btn-info btn-warning btn-danger').addClass('btn-' + label);
};
// ===== MAIN FUNCTIONS =====
const downloadError = (message, error, isServerError) => {
downloadStatus('danger');
handleErrorAlert(message);
if (error) console.error(message, error);
if (isServerError) {
return handleServerError();
}
return handleChapterContentError(message);
};
const handleErrorAlert = (message) => {
if (settings.errorAlert) {
settings.errorAlert = confirm(`Lỗi! ${message}\nBạn có muốn tiếp tục nhận cảnh báo?`);
}
};
const handleServerError = () => {
if (downloadState.delay > 700) {
if (chapterState.current.title) downloadState.errorTitles.push(chapterState.current.title);
console.warn('Dừng tải do quá nhiều lỗi kết nối');
return;
}
downloadStatus('warning');
downloadState.delay += 100;
retryGetContent();
};
const retryGetContent = () => {
setTimeout(async () => {
try {
await getContent();
} catch (error) {
console.error('Lỗi trong retry getContent:', error);
}
}, downloadState.delay);
};
const handleChapterContentError = (message) => {
if (!chapterState.current.title) return;
downloadState.errorTitles.push(chapterState.current.title);
return `<p class="no-indent"><a href="${locationInfo.referrer}${chapterState.current.id}">${message}</a></p>`;
};
const genEbook = async () => {
try {
const epubZipContent = await libs.jepub.generate('blob', (metadata) => {
ui.$download.html('Đang nén <strong>' + metadata.percent.toFixed(2) + '%</strong>');
});
document.title = '[⇓] ' + ebookInfo.title;
window.removeEventListener('beforeunload', beforeleaving);
const ebookFilename = locationInfo.novelAlias + (partState.size ? '-p' + (partState.current + 1) : '') + '.epub';
ui.$download
.attr({
href: window.URL.createObjectURL(epubZipContent),
download: ebookFilename,
})
.text('Hoàn thành')
.off('click');
if (downloadState.status !== 'danger') downloadStatus('success');
saveAs(epubZipContent, ebookFilename);
setTimeout(async () => {
await checkPart();
}, 2000);
} catch (err) {
downloadStatus('danger');
console.error('Lỗi khi tạo EPUB:', err);
ui.$download.text('Lỗi tạo EPUB');
}
};
const checkPart = async () => {
if (partState.current >= partState.size) return;
partState.current++;
chapterState.list = partState.list[partState.current];
chapterState.size = chapterState.list.length;
// Reset chapter state for new part
chapterState.current.id = '';
chapterState.current.title = '';
chapterState.current.index = 0;
chapterState.progress.begin = '';
chapterState.progress.end = '';
downloadState.isFinished = false;
await init();
};
const saveEbook = async () => {
if (downloadState.isFinished) {
console.warn('saveEbook đã được gọi, bỏ qua duplicate call');
return;
}
downloadState.isFinished = true;
ui.$download.html('Bắt đầu tạo EPUB');
console.log('Bắt đầu tạo EPUB...');
let titleErrorHtml = '';
if (downloadState.errorTitles.length) {
titleErrorHtml =
'<p class="no-indent"><strong>Các chương lỗi: </strong>' + downloadState.errorTitles.join(', ') + '</p>';
}
chapterState.progress.summary =
'<p class="no-indent">Nội dung từ <strong>' +
chapterState.progress.begin +
'</strong> đến <strong>' +
chapterState.progress.end +
'</strong></p>';
libs.jepub.notes(chapterState.progress.summary + titleErrorHtml + '<br /><br />' + ebookInfo.credits);
try {
const response = await new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'GET',
url: ebookInfo.cover,
responseType: 'arraybuffer',
onload: resolve,
onerror: reject,
});
});
try {
libs.jepub.cover(response.response);
} catch (err) {
console.error(err);
}
} catch (err) {
console.error('Lỗi khi tải cover:', err);
}
await genEbook();
};
const getContent = async () => {
if (downloadState.isFinished) return;
chapterState.current.id = chapterState.list[chapterState.current.index];
try {
const response = await $.get(locationInfo.pathname + chapterState.current.id + '/');
const $data = $(response);
if (downloadState.isFinished) return;
chapterState.current.title = extractChapterTitle($data);
let chapContent = await processChapterContent($data);
libs.jepub.add(chapterState.current.title, chapContent);
updateChapterProgress();
if (await shouldFinishDownload()) {
await saveEbook();
} else {
scheduleNextChapter();
}
} catch (err) {
handleChapterError(err);
}
};
const processChapterContent = async ($data) => {
const $chapter = $data.find('.chapter-c');
if (!$chapter.length) {
return downloadError('Không có nội dung');
}
await processChapterImages($chapter);
const cleanedContent = cleanChapterContent($chapter);
if (!cleanedContent) {
return downloadError('Nội dung không có');
}
if (downloadState.status !== 'danger') downloadStatus('warning');
return cleanedContent;
};
const updateChapterProgress = () => {
if (chapterState.current.index === 0) chapterState.progress.begin = chapterState.current.title;
chapterState.progress.end = chapterState.current.title;
chapterState.current.index++;
updateDownloadProgress();
};
const shouldFinishDownload = async () => {
const isComplete = chapterState.current.index >= chapterState.size;
if (isComplete) {
console.log('Hoàn thành tải tất cả chương, bắt đầu tạo EPUB...');
}
return isComplete;
};
const scheduleNextChapter = () => {
setTimeout(async () => {
try {
await getContent();
} catch (error) {
console.error('Lỗi trong setTimeout getContent:', error);
downloadError('Lỗi không mong muốn', error, true);
}
}, downloadState.delay);
};
const handleChapterError = (err) => {
console.error('Lỗi khi tải chương:', err);
chapterState.current.title = null;
if (!downloadState.isFinished) {
downloadError('Kết nối không ổn định', err, true);
}
};
const customDownload = () => {
const shouldSplitEbook = confirm('Chọn "OK" nếu muốn chia nhỏ ebook');
if (shouldSplitEbook) {
handleEbookSplitting();
} else {
handleCustomStartChapter();
}
};
const handleEbookSplitting = () => {
const shouldSplitByChapterCount = confirm('Chọn "OK" nếu muốn chia theo số lượng chương');
let chaptersPerPart;
if (shouldSplitByChapterCount) {
chaptersPerPart = getChaptersPerPart();
} else {
chaptersPerPart = getChaptersPerPartByPartCount();
}
if (chaptersPerPart > 0) {
splitChapterList(chaptersPerPart);
}
};
const getChaptersPerPart = () => {
const input = prompt('Nhập số lượng chương mỗi phần:', 2000);
return parseInt(input, 10) || 0;
};
const getChaptersPerPartByPartCount = () => {
const input = prompt('Nhập số phần muốn chia nhỏ:', 3);
const partCount = parseInt(input, 10);
return partCount > 0 ? Math.floor(chapterState.size / partCount) : 0;
};
const splitChapterList = (chaptersPerPart) => {
partState.list = chunkArray(chapterState.list, chaptersPerPart);
partState.size = partState.list.length;
chapterState.list = partState.list[partState.current];
chapterState.size = chapterState.list.length;
};
const handleCustomStartChapter = () => {
const startChapterId = prompt('Nhập ID chương truyện bắt đầu tải:', chapterState.list[0]);
const startIndex = chapterState.list.indexOf(startChapterId);
if (startIndex !== -1) {
chapterState.list = chapterState.list.slice(startIndex);
chapterState.size = chapterState.list.length;
}
};
const init = async () => {
if (!chapterState.size) return;
libs.jepub = new jEpub();
libs.jepub
.init({
title: ebookInfo.title,
author: ebookInfo.author,
publisher: locationInfo.host,
description: ebookInfo.description,
tags: ebookInfo.genres,
})
.uuid(locationInfo.referrer + (partState.size ? '#p' + (partState.current + 1) : ''));
window.addEventListener('beforeunload', beforeleaving);
ui.$download.one('click', async (e) => {
e.preventDefault();
await saveEbook();
});
await getContent();
};
// ===== EXECUTION =====
if (!ui.$novelId.length) return;
const $ebookType = $('.info a[itemprop="genre"]');
if ($ebookType.length)
$ebookType.each(function () {
ebookInfo.genres.push($(this).text().trim());
});
ui.$download.insertAfter('.info');
ui.$download.wrap('<div class="panel-group books"></div>');
ui.$download.one('click contextmenu', async (e) => {
e.preventDefault();
document.title = '[...] Vui lòng chờ trong giây lát';
try {
const res = await $.get('/ajax.php', {
type: 'hash',
});
try {
const data = await $.get('/ajax.php', {
type: 'chapter_option',
data: ui.$novelId.val(),
bnum: '',
num: 1,
hash: res,
});
chapterState.list = data.match(/(?:value=")[^"]+(?=")/g).map((val) => {
return val.slice(7);
});
chapterState.size = chapterState.list.length;
if (e.type === 'contextmenu') {
ui.$download.off('click');
customDownload();
} else {
ui.$download.off('contextmenu');
}
await init();
} catch (jqXHR) {
downloadError(jqXHR.statusText || 'Lỗi tải danh sách chương');
}
} catch (jqXHR) {
ui.$download.text('Lỗi danh mục');
downloadStatus('danger');
console.error(jqXHR);
}
});
})(jQuery, window, document);