Tải truyện từ Vozer định dạng EPUB.
// ==UserScript== // @name Vozer downloader // @namespace https://lelinhtinh.github.io/ // @description Tải truyện từ Vozer định dạng EPUB. // @version 1.0.0 // @icon https://raw.githubusercontent.com/lelinhtinh/Userscript/refs/heads/master/vozer_downloader/icon.jpg // @author lelinhtinh // @oujs:author baivong // @license MIT; https://lelinhtinh.mit-license.org/license.txt // @match https://vozer.io/* // @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 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 chapId = chapterState.current.link.replace(/\W+/g, '_'); const imageId = await downloadAndAddImage(absoluteUrl, `chap_${chapId}_img_${imageIndex}`); imgElement.replaceWith(`<p><%= image['${imageId}'] %></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('<p><a href="' + imgSrc + '">Click để xem ảnh</a></p>'); } }; 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'; }); const $textNodes = $chapter.contents().filter(function () { return this.nodeType === 3 && this.nodeValue.trim() !== ''; }); $unwantedElements.remove(); $hiddenElements.remove(); $textNodes.remove(); return $chapter.text().trim() !== '' ? cleanHtml($chapter.html()) : null; }; const extractChapterTitle = ($data) => { let title = $data.find('h1').text().trim(); if (!title) { const chapterMatch = chapterState.current.link.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: null, cover: null, description: null, genres: [], credits: `<p>Truyện được tải từ <a href="${locationInfo.referrer}">Vozer</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: { link: '', 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: 'pt-1.5 pb-1 px-2 ml-4 leading-normal font-semibold text-white rounded bg-blue-001', href: '#download', text: 'Tải xuống', }), $description: $('#chapter_001 > .font-content.smiley'), }; // External Libraries const libs = { jepub: null, }; // Helper function for download status const downloadStatus = (label) => { const labelStatus = { primary: 'bg-blue-001', success: 'bg-green-001', danger: 'bg-red-001', warning: 'bg-yellow-600', }; downloadState.status = label; ui.$download .removeClass('bg-blue-001 bg-green-001 bg-red-001 bg-yellow-600') .addClass(labelStatus[label] || 'bg-blue-001'); }; // ===== 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="${chapterState.current.link}">${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.link = ''; 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.link = chapterState.list[chapterState.current.index]; try { const response = await $.get(chapterState.current.link); 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('#content'); 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 crawlChapterList = async ($document = $(document), chapterLinks = []) => { const $chapterLinks = $document.find('td.text-blue-001 > a'); if (!$chapterLinks.length) return chapterLinks; chapterLinks.push(...$chapterLinks.map((_, link) => $(link).attr('href').trim())); const $nextPage = $document.find('[rel="next"]'); if ($nextPage.length) { const nextPageUrl = $nextPage.attr('href').trim(); console.log('Đang tải danh sách chương:', nextPageUrl); const nextPageData = await $.get(nextPageUrl); return crawlChapterList($(nextPageData), chapterLinks); } return chapterLinks; }; 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.$description.length) return; const $bookData = $('#chapter_001').prev('script'); if (!$bookData.length) return; let bookData = JSON.parse($bookData.text().trim()); bookData = bookData['@graph']?.find((i) => i['@type'] === 'Book'); if (!bookData) return; ebookInfo.author = bookData.author?.name || null; ebookInfo.cover = bookData.image || null; ebookInfo.description = ui.$description.html().trim() || null; ebookInfo.genres = [bookData.genre || null]; $('#chapter_001 > div.border > div.text-justify > div').append(ui.$download); ui.$download.one('click contextmenu', async (e) => { e.preventDefault(); document.title = '[...] Vui lòng chờ trong giây lát'; try { chapterState.list = await crawlChapterList(); 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'); } }); })(jQuery, window, document);