/* eslint-disable no-multi-spaces */ /* eslint-disable no-return-assign */ // ==UserScript== // @name Pixiv novel to Epub // @name:zh-CN Pixiv小说Epub合成器 // @name:en Pixiv novel to Epub // @namespace PY-DNG userscripts // @version 0.1.8 // @description Download pixiv novels in Epub format // @description:zh-CN 以Epub格式下载Pixiv小说 // @description:en Download pixiv novels in Epub format // @author PY-DNG // @license GPL-3.0-or-later // @match *://www.pixiv.net/* // @match *://pixiv.net/* // @connect pximg.net // @require https://update.greasyfork.icu/scripts/456034/1348286/Basic%20Functions%20%28For%20userscripts%29.js // @require data:application/javascript,window.setImmediate%20%3D%20window.setImmediate%20%7C%7C%20((f%2C%20...args)%20%3D%3E%20window.setTimeout(()%20%3D%3E%20f(args)%2C%200))%3B // @require https://fastly.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js // @require https://fastly.jsdelivr.net/npm/ejs@3.1.9/ejs.min.js // @require https://fastly.jsdelivr.net/npm/jepub@2.1.4/dist/jepub.min.js // @icon data:image/png;base64,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 // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @run-at document-start // @downloadURL https://update.greasyfork.icu/scripts/483999/Pixiv%20novel%20to%20Epub.user.js // @updateURL https://update.greasyfork.icu/scripts/483999/Pixiv%20novel%20to%20Epub.meta.js // ==/UserScript== // @require https://fastly.jsdelivr.net/npm/setimmediate@1.0.5/setImmediate.min.js // @require https://fastly.jsdelivr.net/npm/jepub@2.1.4/dist/jepub.min.js // @require https://fastly.jsdelivr.net/npm/ejs@3.1.9/ejs.min.js // @require https://fastly.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js /* global LogLevel DoLog Err $ $All $CrE $AEL $$CrE addStyle detectDom destroyEvent copyProp copyProps parseArgs escJsStr replaceText getUrlArgv dl_browser dl_GM AsyncManager queueTask testChecker registerChecker loadFuncs */ /* global jEpub, JSZip, ejs */ let PixivAPI = (function() { queueTask.GM_xmlhttpRequest = { sleep: 200, max: 10 }; return { get, safeGet, utils: { toAbsURL, toSearch, queueTask }, // https://www.pixiv.net/ajax/novel/18673574 novel: id => safeGet(`/ajax/novel/${id}`), // https://www.pixiv.net/ajax/novel/7522350/insert_illusts?id%5B%5D=60139778-1&lang=zh&version=1efff679631a40a674235820806f7431d67065d9 insert_illusts: (novel_id, illust_ids, lang='zh') => { const url = `/ajax/novel/${novel_id}/insert_illusts`; const query = { lang }; if (Array.isArray(illust_ids)) { for (let i = 0; i < illust_ids.length; i++) { const id = illust_ids[i]; query[`id[${i}]`] = id; } } else { query[`id[]`] = illust_ids; } return safeGet(url, query); }, // https://www.pixiv.net/ajax/novel/series/9649276?lang=zh&version=a48f2f681629909b885608393916b81989accf5b // 'version' removed due to unspecified meaning series: (id, lang='zh') => safeGet(`/ajax/novel/series/${id}`, { id, lang }), // https://www.pixiv.net/ajax/novel/series_content/9649276?limit=30&last_order=0&order_by=asc series_content: (id, limit=30, last_order=0, order_by='asc') => safeGet(`/ajax/novel/series_content/${id}`, { limit, last_order, order_by }), }; function safeGet() { return queueTask(() => get.call(this, ...arguments), 'GM_xmlhttpRequest'); } function get(url, params, responseType='json', retry=2) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', responseType, headers: { Referer: /^(www\.)?pixiv\.net$/.test(location.host) ? location.href : 'https://www.pixiv.net/' }, url: toAbsURL(url, params), onload: async res => res.status === 200 && (responseType !== 'json' || res.response?.error === false) ? resolve(res.response) : checkRetry(res), onerror: checkRetry }); async function checkRetry(err) { retry-- > 0 ? resolve(await get(url, params, responseType, retry)) : reject(err); } }); } function toAbsURL(pathname, searchOptions) { return new URL(pathname, `https://www.pixiv.net/`).href + (searchOptions ? `?${toSearch(searchOptions)}` : ''); } function toSearch(options) { return new URLSearchParams(options).toString() } }) (); (async function __MAIN__() { 'use strict'; const CONST = { TextAllLang: { DEFAULT: 'zh-CN', 'zh-CN': { DownloadEpub: '下载当前小说Epub', DownloadEpub_Short: '下载Epub', DownloadEpub_Progress: 'Epub (C/A)', DownloadComplete: 'Epub下载完成', RestrictData: {"0":"Enable","1":"NotFound","2":"Mypixiv","3":"R18","4":"R18G","Enable":0,"NotFound":1,"Mypixiv":2,"R18":3,"R18G":4}, RestrictInfo: { NotFound: "#%(order)は非公開作品です", // No translation provided by pixiv yet Mypixiv: '#%(order)是好P友限定作品', R18: '#%(order)是R-18作品', R18G: '#%(order)是R-18G作品' }, UnvieableTitle: '该章节无法查看', // unused constance, deletable UnvieableContent: '此章节Pixiv并未开放查看,请到Pixiv网站或app检查该章节是否设置了阅读限制\n如果是R18/R18G阅读限制,可到Pixiv网站打开R18/R18G开关' } }, GFURL: 'https://greasyfork.org/scripts/483999', GFAuthorURL: 'https://greasyfork.org/users/667968', Symbol: { CHAPTER_NOT_VIEWABLE: Symbol('CHAPTER_NOT_VIEWABLE') } }; // Init language const i18n = Object.keys(CONST.TextAllLang).includes(navigator.language) ? navigator.language : CONST.TextAllLang.DEFAULT; CONST.Text = CONST.TextAllLang[i18n]; // @require fallbacks await Promise.all([ { missing: typeof setImmediate === 'undefined', src: 'https://fastly.jsdelivr.net/npm/setimmediate@1.0.5/setImmediate.min.js' }, { missing: typeof JSZip === 'undefined', src: 'https://fastly.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js' }, { missing: typeof ejs === 'undefined', src: 'https://fastly.jsdelivr.net/npm/ejs@3.1.9/ejs.min.js' }, { missing: typeof jEpub === 'undefined', src: 'https://fastly.jsdelivr.net/npm/jepub@2.1.4/dist/jepub.min.js' } ].filter(script => script.missing).map(src => new Promise((resolve, reject) => document.head.appendChild($$CrE({ tagName: 'script', props: { src }, listeners: [ ['load', resolve], ['error', reject] ] }))))); // Progress const progress = { __finished: true, // {boolean} All tasks finished __cur: 0, // {number} current task number __all: 0, // {number} all tasks count __listeners: {}, __id: 0, start() { this.__finished = false; this.__cur = 0; this.__all = 0; Object.values(this.__listeners).forEach(l => l(this.__cur, this.__all, this.__finished)); }, finish() { this.__finished = true; Object.values(this.__listeners).forEach(l => l(this.__cur, this.__all, this.__finished)); }, update(cur, all=false) { this.__cur = cur; all !== false && (this.__all = all); Object.values(this.__listeners).forEach(l => l(cur, all, this.__finished)); }, listen(l) { const id = this.__id++; this.__listeners[id] = l; return id; }, remove(id) { delete this.__listeners[id] }, get finished() { return this.__finished; }, get cur() { return this.__cur; }, get all() { return this.__all; }, get listeners() { return this.__listeners; } }; // User Interface GM_registerMenuCommand(CONST.Text.DownloadEpub, downloadEpub); loadFuncs([{ func: () => { detectDom({ selector: 'main>section section', callback: section => { if (!testChecker({ type: 'regpath', value: [ /^\/novel\/show\.php$/, /^\/novel\/series\/\d+$/ ] })) { return; } const toolbar = section; const dlDiv = makeDownloadButton(); toolbar.appendChild(dlDiv); } }); function makeDownloadButton() { const DOWNLOAD = ''; const CANCEL = '' const STYLE = '.epub-download { margin-right: 20px; line-height: 32px; font-weight: 700; cursor: pointer; padding: 0px; background: none; border: none; } .epub-download-button { display: inline-block; padding: 0; color: inherit; background: none; height: 32px; line-height: 32px; border: none; font-weight: 700; cursor: pointer; } .epub-download-svg { vertical-align: middle; overflow: visible !important; margin-right: 4px; width: 12px; font-size: 0; -webkit-transition: fill .2s; transition: fill .2s; fill: currentColor; } .epub-download-span { vertical-align: middle; }'; addStyle(STYLE, 'novel-epub-download'); const div = $$CrE({ tagName: 'div', classes: 'epub-download', //listeners: [['click', e => download]] }); const button = $$CrE({ tagName: 'button', classes: 'epub-download-button', props: { innerHTML: DOWNLOAD } }); const span = $$CrE({ tagName: 'span', classes: 'epub-download-span', props: { innerText: CONST.Text.DownloadEpub_Short } }); $AEL(div, 'click', download); button.appendChild(span); div.appendChild(button); return div; function download() { if (!progress.finished) { return; } const listernerID = progress.listen((cur, all, finished) => { if (finished) { span.innerText = CONST.Text.DownloadComplete; progress.remove(listernerID); } else { const text = replaceText(CONST.Text.DownloadEpub_Progress, { C: cur, A: all }); span.innerText = text; } }) downloadEpub(); } } } }]) function downloadEpub() { const pathname = location.pathname; // Novel series // https://www.pixiv.net/novel/series/9649276 /^\/novel\/series\/\d+$/.test(pathname) && downloadSeries(); // Novel // https://www.pixiv.net/novel/show.php?id=18673574 /^\/novel\/show\.php$/.test(pathname) && downloadNovel(); } async function downloadSeries() { DoLog('Start download series'); progress.start(); progress.update(1, 5); const id = location.pathname.split('/').pop(); const epub = new jEpub(); // Get series data const series = (await PixivAPI.series(id)).body; await initEpub(epub, series); // List all novels progress.update(2, 5); const promises = []; for (let index = 0; index < series.total; index += 30) { const promise = PixivAPI.series_content(id, 30, index); promises.push(promise); } const list = (await Promise.all(promises)).reduce((l, json) => ((l.push(...json.body.page.seriesContents), l)), []); DoLog(list); progress.update(3, 5); const novel_datas = await Promise.all(list.map( async novel => ( novel.series.viewableType == 0 ? ( (await PixivAPI.novel(novel.id)).body ) : { unviewable: CONST.Symbol.CHAPTER_NOT_VIEWABLE, series: novel.series } ) )); DoLog(novel_datas); /* Now loading chapter and adding loaded chapter has been separated, use Promise.all to speed up the process // Add chapters one by one // Do not use promise.all, because that will break the order for (const data of novel_datas) { await addChapter(epub, data); } //await Promise.all(novel_datas.map(async data => await addChapter(epub, data))); */ // Load all chapters asynchronously and Add them to epub at once progress.update(4, 5); const chapters = await Promise.all(novel_datas.map(data => loadChapter(epub, data))); chapters.forEach(chapter => addLoaded(epub, chapter)); DoLog(chapters); DoLog('Saving Epub'); progress.update(5, 5); saveEpub(epub, series.title + '.epub', () => progress.finish()); } async function downloadNovel() { DoLog('Start download novel'); progress.start(); progress.update(1, 2); const id = getUrlArgv('id'); const json = await PixivAPI.novel(id); const data = json.body; const epub = new jEpub(); await Promise.all([initEpub(epub, data), addChapter(epub, data)]); progress.update(2, 2); saveEpub(epub, data.title + '.epub', () => progress.finish()); } // Compatible with PixivAPI.novel / PixivAPI.series async function initEpub(epub, data) { const html_link = `${htmlEncode(location.href)}`; const html_desc = `
${htmlEncode(CONST.Text.UnvieableContent).replace('\n', '
')}
s return subContent.split('\n').map(line => line.trim() ? `
${line}
` : '${line}
`) .join('\n'); description = `