TruyenFull downloader

Tải truyện từ TruyenFull định dạng EPUB.

Install this script?
Author's suggested script

You may also like truyenfull.com downloader.

Install this script
// ==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);