🎉 欢迎访问GreasyFork.Org 镜像站!本镜像站由公众号【爱吃馍】搭建,用于分享脚本。联系邮箱📮

Greasy fork 爱吃馍镜像

Greasy Fork is available in English.

cmoa.jp Downloader

Downloads comic pages from cmoa.jp

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

// ==UserScript==
// @name         cmoa.jp Downloader
// @version      1.1.5
// @description  Downloads comic pages from cmoa.jp
// @author       tnt_kitty
// @match        *://*.cmoa.jp/bib/speedreader/*
// @icon         https://www.cmoa.jp/favicon.ico
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @grant        GM_download
// @resource     bt https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/js/all.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
// @license      GPL-3.0-only
// @namespace    https://greasyfork.org/users/914763
// ==/UserScript==

function convertToValidFileName(string) {
    return string.replace(/[/\\?%*:|"<>]/g, '-');
}

function isValidFileName(string) {
    const regex = new RegExp('[/\\?%*:|"<>]', 'g');
    return !regex.test(string);
}

function getTitle() {
    try {
        return __sreaderFunc__.contentInfo.items[0].Title;
    } catch (error) {
        return null;
    }
}

function getAuthors() {
    try {
        return __sreaderFunc__.contentInfo.items[0].Authors[0].Name.split('/'); // Returns array of authors, ex. ['Author1', 'Author2']
    } catch (error) {
        return null;
    }
}

function getVolume() {
    try {
        return parseInt(__sreaderFunc__.contentInfo.items[0].ShopURL.split('/').at(-2));
    } catch (error) {
        return null;
    }
}

function getPageCount() {
    try {
        return SpeedBinb.getInstance('content').total;
    } catch (error) {
        return null;
    }
}

function getPageIntervals() {
    const isEmpty = string => !string.trim().length;

    const pagesField = document.querySelector('#pages-field');
    let fieldValue = pagesField.value;

    if (isEmpty(fieldValue)) {
        const speedbinb = SpeedBinb.getInstance('content');
        const totalPages = getPageCount();
        return [[1, totalPages]];
    }

    const pagesList = fieldValue.split(',');
    let pageIntervals = [];

    for (const x of pagesList) {
        let pages = x.split('-');
        if (pages.length === 1) {
            pageIntervals.push([parseInt(pages[0]), parseInt(pages[0])]);
        } else if (pages.length === 2) {
            pageIntervals.push([parseInt(pages[0]), parseInt(pages[1])]);
        }
    }

    if (pageIntervals.length <= 1) {
        return pageIntervals;
    }

    pageIntervals.sort((a, b) => b[0] - a[0]);

    const start = 0, end = 1;
    let mergedIntervals = [];
    let newInterval = pageIntervals[0];
    for (let i = 1; i < pageIntervals.length; i++) {
        let currentInterval = pageIntervals[i];
        if (currentInterval[start] <= newInterval[end]) {
            newInterval[end] = Math.max(newInterval[end], currentInterval[end]);
        } else {
            mergedIntervals.push(newInterval);
            newInterval = currentInterval;
        }
    }
    mergedIntervals.push(newInterval);
    return mergedIntervals;
}

function initializeComicInfo() {
    const titleListItem = document.querySelector('#comic-title');
    const authorListItem = document.querySelector('#comic-author');
    const volumeListItem = document.querySelector('#comic-volume');
    const pageCountListItem = document.querySelector('#comic-page-count');

    const titleDiv = document.createElement('div');
    titleDiv.innerText = getTitle();
    titleListItem.appendChild(titleDiv);

    const authors = getAuthors();
    if (authors.length > 1) {
        const authorLabel = authorListItem.querySelector('.fw-bold');
        authorLabel.innerText = 'Authors';
    }
    for (let i = 0; i < authors.length; i++) {
        const authorDiv = document.createElement('div');
        authorDiv.innerText = authors[i];
        authorListItem.appendChild(authorDiv);
    }

    const volumeDiv = document.createElement('div');
    volumeDiv.innerText = getVolume();
    volumeListItem.appendChild(volumeDiv);

    const pageCountDiv = document.createElement('div');
    pageCountDiv.innerText = getPageCount();
    pageCountListItem.appendChild(pageCountDiv);
}

function initializeDownloadName() {
    const downloadNameField = document.querySelector('#download-name-field');
    downloadNameField.placeholder = convertToValidFileName(getTitle().concat(' ', getVolume()));
}

function initializeSidebar() {
    initializeComicInfo();
    initializeDownloadName();

    const speedbinb = SpeedBinb.getInstance('content');
    speedbinb.removeEventListener('onPageRendered', initializeSidebar); // Remove event listener to prevent info from being added again
}

function validateDownloadNameField() {
    const downloadNameField = document.querySelector('#download-name-field');
    if (isValidFileName(downloadNameField.value)) {
        downloadNameField.setCustomValidity('');
    } else {
        downloadNameField.setCustomValidity('Special characters /\?%*:|"<>] are not allowed');
    }
}

function validatePagesField() {
    const totalPages = getPageCount();

    const pagesField = document.querySelector('#pages-field');
    const fieldValue = pagesField.value;
    const pagesList = fieldValue.split(',');

    const isValidPage = num => !isNaN(num) && (parseInt(num) > 0) && (parseInt(num) <= totalPages);
    const isValidSingle = range => (range.length === 1) && isValidPage(range[0]);
    const isValidRange = range => (range.length === 2) && range.every(isValidPage) && (parseInt(range[0]) < parseInt(range[1]));

    for (const x of pagesList) {
        let pages = x.split('-');
        if (!isValidSingle(pages) && !isValidRange(pages)) {
            pagesField.setCustomValidity('Invalid page range, use eg. 1-5, 8, 11-13 or leave blank');
            return;
        }
    }
    pagesField.setCustomValidity('');
}

function preventDefaultValidation() {
  'use strict'

  // Fetch all the forms we want to apply custom Bootstrap validation styles to
  var forms = document.querySelectorAll('.needs-validation');

  // Loop over them and prevent submission
  Array.prototype.slice.call(forms)
      .forEach(function (form) {
          form.addEventListener('submit', function (event) {
              if (!form.checkValidity()) {
                  event.preventDefault();
                  event.stopPropagation();
              } else {
                  submitForm(event);
              }
              form.classList.add('was-validated');
      }, false)
    });
}

function submitForm(e) {
    e.preventDefault();
    const downloadNameField = document.querySelector('#download-name-field');
    if (!downloadNameField.value) {
        downloadNameField.value = downloadNameField.placeholder;
    }
    const form = document.querySelector('#download-sidebar form');
    const elements = form.elements;
    for (let i = 0; i < elements.length; i++) {
        elements[i].readOnly = true;
    }
    const downloadButton = document.querySelector('#download-button');
    downloadButton.disabled = true;
    downloadComic(getPageIntervals());
}

function setUpDownloadForm() {
    const pagesField = document.querySelector('#pages-field');
    pagesField.addEventListener('change', validatePagesField);

    const downloadNameField = document.querySelector('#download-name-field');
    downloadNameField.addEventListener('change', validateDownloadNameField);

    preventDefaultValidation();
}

function addSidebarEventListeners() {
    const stopProp = function(e) { e.stopPropagation(); };
    const sidebar = document.querySelector('#download-sidebar');
    sidebar.addEventListener('shown.bs.offcanvas', function() {
        document.addEventListener('keydown', stopProp, true);
        document.addEventListener('wheel', stopProp, true);
    });
    sidebar.addEventListener('hidden.bs.offcanvas', function() {
        document.removeEventListener('keydown', stopProp, true);
        document.removeEventListener('wheel', stopProp, true);
    });
}

function getImgCoordinates(img, pageWidth, pageHeight) {
    const insetTop = parseFloat(img.parentElement.style.top);
    const insetRight = parseFloat(img.parentElement.style.right);
    const insetBottom = parseFloat(img.parentElement.style.bottom);
    const insetLeft = parseFloat(img.parentElement.style.left);

    return {
        x: (pageHeight * insetLeft) / 100,
        y: (pageHeight * insetTop) / 100,
        width: pageWidth * ((100 - insetRight - insetLeft) / 100),
        height: pageHeight * ((100 - insetTop - insetBottom) / 100),
    };
}

function getPageBlob(pageNumber, scaled) {
    return new Promise(function(resolve, reject) {
        const speedbinb = SpeedBinb.getInstance('content');
        const pageInfo = speedbinb.Ii.Hn.page;
        const orgPageHeight = pageInfo[pageNumber - 1].image.orgheight;
        const orgPageWidth = pageInfo[pageNumber - 1].image.orgwidth;

        const imgs = document.getElementById(`content-p${pageNumber}`).getElementsByTagName('img');

        const imgsArray = Array.from(imgs);
        const pageWidth = scaled ? orgPageWidth : imgsArray[0].naturalWidth;

        const pageHeight = scaled ? orgPageHeight : Math.floor(orgPageHeight * pageWidth / orgPageWidth);

        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        canvas.height = pageHeight;
        canvas.width = pageWidth;

        const topImgCoordinates = getImgCoordinates(imgsArray[0], pageWidth, pageHeight);
        const middleImgCoordinates = getImgCoordinates(imgsArray[1], pageWidth, pageHeight);
        const bottomImgCoordinates = getImgCoordinates(imgsArray[2], pageWidth, pageHeight);

        ctx.drawImage(imgs[0], topImgCoordinates.x, topImgCoordinates.y, topImgCoordinates.width, topImgCoordinates.height);
        ctx.drawImage(imgs[1], middleImgCoordinates.x, middleImgCoordinates.y, middleImgCoordinates.width, middleImgCoordinates.height);
        ctx.drawImage(imgs[2], bottomImgCoordinates.x, bottomImgCoordinates.y, bottomImgCoordinates.width, bottomImgCoordinates.height);

        canvas.toBlob(blob => { resolve(blob); }, 'image/jpeg', 1.0);
    });
}

async function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

async function waitUntilPageLoaded(pageNumber) {
    const speedbinb = SpeedBinb.getInstance('content');
    speedbinb.moveTo(pageNumber - 1);
    while (!document.getElementById(`content-p${pageNumber}`)) {
        await sleep(200);
    }
    while (!document.getElementById(`content-p${pageNumber}`).getElementsByTagName('img')) {
        await sleep(200);
    }
    while (document.getElementById(`content-p${pageNumber}`).getElementsByTagName('img').length !== 3) {
        await sleep(200);
    }
    const imgs = document.getElementById(`content-p${pageNumber}`).getElementsByTagName('img');
    for (let i = 0; i < imgs.length; i++) {
        while (!imgs[i].complete) {
            await sleep(200);
        }
    }
    return new Promise(function(resolve, reject) {
        resolve();
    });
}

function toggleProgressBar() {
    const progress = document.querySelector('#download-sidebar .progress');
    const progressBar = document.querySelector('#download-sidebar .progress-bar');

    if (progress.classList.contains('invisible')) {
        progress.classList.remove('invisible');
        progress.classList.add('visible');
        progressBar.style.width = '0%';
    } else if (progress.classList.contains('visible')) {
        progress.classList.remove('visible');
        progress.classList.add('invisible');
        progressBar.style.width = '0%';
    }
}

function updateProgressBar(percentage) {
    const progressBar = document.querySelector('#download-sidebar .progress-bar');
    progressBar.style.width = `${percentage}%`;
}

async function downloadComic(pageIntervals) {
    const stopProp = function(e) { e.preventDefault(); e.stopPropagation(); };
    const sidebar = document.querySelector('#download-sidebar');
    sidebar.addEventListener('hide.bs.offcanvas', stopProp, true);

    const zip = new JSZip();
    const downloadName = document.querySelector('#download-name-field').value;
    const shouldScalePages = document.querySelector('#scale-checkbox').checked;

    toggleProgressBar();

    let totalPages = 0;
    for (let i = 0; i < pageIntervals.length; i++) {
        totalPages += pageIntervals[i][1] - pageIntervals[i][0];
    }

    let downloadedPages = 0;
    const speedbinb = SpeedBinb.getInstance('content');

    for (let i = 0; i < pageIntervals.length; i++) {
        const interval = pageIntervals[i], start = 0, end = 1;
        for (let nextPage = interval[start]; nextPage <= interval[end]; nextPage++) {
            await waitUntilPageLoaded(nextPage);
            const pageBlob = await getPageBlob(nextPage, shouldScalePages);
            zip.file(`${nextPage}.jpeg`, pageBlob);
            downloadedPages++;
            updateProgressBar(Math.round((downloadedPages / totalPages) * 100));
        }
    }

    zip.generateAsync({ type: 'blob' }, function updateCallback(metadata) {
        updateProgressBar(Math.round(metadata.percent));
    }).then(function(content) {
        const details = {
            'url': URL.createObjectURL(content),
            'name': `${downloadName}.zip`
        };
        GM_download(details);

        toggleProgressBar();

        const form = document.querySelector('#download-sidebar form');
        const elements = form.elements;
        for (let i = 0; i < elements.length; i++) {
            elements[i].readOnly = false;
        }

        const downloadButton = document.querySelector('#download-button');
        downloadButton.disabled = false;

        sidebar.removeEventListener('hide.bs.offcanvas', stopProp, true);
    });
}

function addDownloadTab() {
    const tabAnchor = document.createElement('a');
    tabAnchor.id = 'download-tab-anchor';
    tabAnchor.setAttribute('data-bs-toggle', 'offcanvas')
    tabAnchor.setAttribute('href', '#download-sidebar');
    tabAnchor.setAttribute('role', 'button');
    tabAnchor.setAttribute('aria-label', 'Open Download Options');

    const tab = document.createElement('div');
    tab.id = 'download-tab';
    tab.classList.add('rounded-start');

    const icon = document.createElement('i');
    icon.id = 'download-icon';
    icon.classList.add('fas');
    icon.classList.add('fa-file-download');

    tabAnchor.appendChild(tab);
    tab.appendChild(icon);
    document.body.append(tabAnchor);

    const tabCss =
    `#download-tab {
         background-color: var(--bs-orange);
         color: white;
         position: absolute;
         top: 3em;
         right: 0;
         z-index: 20;
         padding: 0.75em;
     }
     #download-tab:hover {
         background-color: #ca6510;
     }`;
    GM_addStyle(tabCss);
}

function addDownloadSidebar() {
    const sidebar = document.createElement('div');
    sidebar.id = 'download-sidebar';
    sidebar.classList.add('offcanvas');
    sidebar.classList.add('offcanvas-end');
    sidebar.classList.add('rounded-start');
    sidebar.setAttribute('tabindex', '-1');
    sidebar.setAttribute('aria-labelledby', '#download-sidebar-title');

    sidebar.innerHTML = `
<div class="offcanvas-header">
    <h5 id="download-sidebar-title">Download Options</h5>
    <button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
    <div class="alert alert-warning d-flex align-items-center" role="alert">
        <i class="fas fa-exclamation-triangle bi flex-shrink-0 me-2" width="24" height="24" role="img" aria-label="Warning"></i>
        <div id="warning" style="padding-left: 0.5em">Do not interact with the reader while download is in progress.</div>
    </div>
    <ul class="list-group mb-3">
        <li class="list-group-item" id="comic-title">
            <div class="fw-bold">Title</div>
        </li>
        <li class="list-group-item" id="comic-author">
            <div class="fw-bold">Author</div>
        </li>
        <li class="list-group-item" id="comic-volume">
            <div class="fw-bold">Volume</div>
        </li>
        <li class="list-group-item" id="comic-page-count">
            <div class="fw-bold">Page Count</div>
        </li>
    </ul>
    <form id="download-options-form" class="needs-validation" novalidate>
        <div class="mb-3">
            <label for="download-name-field" class="form-label">Download name</label>
            <textarea type="text" id="download-name-field" name="download-name" class="form-control" placeholder="Leave blank for comic name"></textarea>
            <div class="invalid-feedback">Special characters /\?%*:|"&lt;&gt;] are not allowed</div>
        </div>
        <div class="mb-3">
            <label for="pages-field" class="form-label">Pages</label>
            <input type="text" id="pages-field" name="pages" class="form-control" placeholder="eg. 1-5, 8, 11-13">
            <div class="invalid-feedback">Invalid page range, use eg. 1-5, 8, 11-13</div>
        </div>
        <div class="form-check d-flex align-items-center">
            <input class="form-check-input me-2" type="checkbox" value="" id="scale-checkbox">
            <label class="form-check-label me-2" for="scale-checkbox">Scale pages that are different sizes</label>
            <a class="btn p-0" data-bs-toggle="collapse" href="#scale-checkbox-info" role="button" aria-expanded="false" aria-controls="scaleCheckboxInfo">
                <i class="fas fa-info-circle" width="24" height="24" aria-label="Info"></i>
            </a>
        </div>
        <div class="collapse" id="scale-checkbox-info">
            <div class="card card-body mt-2">
                cmoa may send pages that are a different size than the rest. If you select this option, those pages will be automatically resized. This may affect the image quality.
            </div>
        </div>
    </form>
</div>
<div id="sidebar-footer" class="footer d-flex align-content-center position-absolute bottom-0 start-0 p-3">
    <button type="submit" form="download-options-form" id="download-button" class="btn btn-primary">Download</button>
    <div class="progress ms-3 invisible" style="flex-grow: 1">
        <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
    </div>
</div>`;
    document.body.append(sidebar);
    setUpDownloadForm();
    addSidebarEventListeners();

    const sidebarCss =
    `#download-sidebar {
         user-select: text;
         -moz-user-select: text;
         -webkit-user-select: text;
         -ms-user-select: text;
     }
     #download-sidebar .offcanvas-header {
         border-bottom: 1px solid var(--bs-gray-300);
     }
     #download-sidebar h5 {
         margin-bottom: 0;
     }
     #sidebar-footer {
         border-top: 1px solid var(--bs-gray-300);
         width: 100%;
     }
     .offcanvas-body {
         margin-bottom: 71px;
     }`;
    GM_addStyle(sidebarCss);
}

window.addEventListener('load', () => {
    GM_addStyle(GM_getResourceText("bt"));
    addDownloadSidebar();
    addDownloadTab();
    const speedbinb = SpeedBinb.getInstance('content');
    speedbinb.addEventListener('onPageRendered', initializeSidebar);
});