diff --git a/docs/anime.md b/docs/anime.md index 8e897496f..8232fb643 100644 --- a/docs/anime.md +++ b/docs/anime.md @@ -246,6 +246,12 @@ pageClass: routes 见 [#bilibili](/social-media.html#bilibili) +## CCC 創作集 + +### 漫畫 + + + ## DLsite ### 当前日期发售的新产品 diff --git a/lib/v2/creative-comic/book.js b/lib/v2/creative-comic/book.js new file mode 100644 index 000000000..a14773b05 --- /dev/null +++ b/lib/v2/creative-comic/book.js @@ -0,0 +1,60 @@ +const { parseDate } = require('@/utils/parse-date'); +const { art } = require('@/utils/render'); +const path = require('path'); +const { getUuid, getBook, getChapter, getChapters, getImgEncrypted, getImgKey, decrypt, getRealKey, siteHost } = require('./utils'); + +module.exports = async (ctx) => { + const { id, coverOnly = 'true', quality = '1' } = ctx.params; + const uuid = await getUuid(ctx.cache.tryGet); + let { data: book } = await getBook(id, uuid); + let { data: chapters } = await getChapters(id, uuid); + book = book.data; + chapters = chapters.data; + + const items = await Promise.all( + chapters.chapters.map(async (c) => { + let pages; + if (coverOnly !== 'true' && coverOnly !== '1') { + let { data: chapter } = await getChapter(c.id, uuid); + chapter = chapter.data; + + if (chapter.chapter.free_day === null || chapter.chapter.free_day === 0) { + pages = await Promise.all( + chapter.chapter.proportion.map(async (p) => { + let { data: imgKey } = await getImgKey(p.id, uuid); + imgKey = imgKey.data.key; + + const realKey = getRealKey(imgKey); + const encrypted = await getImgEncrypted(p.id, quality); + + return ctx.cache.tryGet(`${siteHost}/fs/chapter_content/encrypt/${p.id}/${quality}`, () => decrypt(encrypted, realKey)); + }) + ); + } + } + + return { + title: c.vol_name, + description: art(path.join(__dirname, 'templates/chapter.art'), { + chapter: c, + pages, + cover: c.image1, + }), + pubDate: parseDate(c.online_at), + updated: parseDate(c.updated_at), + link: `https://www.creative-comic.tw/reader_comic/${c.id}`, + author: book.author.map((author) => author.name).join(', '), + category: book.tags.map((tag) => tag.name), + }; + }) + ); + + ctx.state.data = { + title: `${book.name} | CCC創作集`, + description: `${book.brief} ${book.description}`, + link: book.share_link, + image: book.image1, + item: items, + language: 'zh-hant', + }; +}; diff --git a/lib/v2/creative-comic/maintainer.js b/lib/v2/creative-comic/maintainer.js new file mode 100644 index 000000000..3dcaecb97 --- /dev/null +++ b/lib/v2/creative-comic/maintainer.js @@ -0,0 +1,3 @@ +module.exports = { + '/book/:id/:coverOnly?/:quality?': ['TonyRL'], +}; diff --git a/lib/v2/creative-comic/radar.js b/lib/v2/creative-comic/radar.js new file mode 100644 index 000000000..9e7af7531 --- /dev/null +++ b/lib/v2/creative-comic/radar.js @@ -0,0 +1,13 @@ +module.exports = { + 'creative-comic.tw': { + _name: 'CCC 創作集', + '.': [ + { + title: '漫畫', + docs: 'https://docs.rsshub.app/anime.html#ccc-chuang-zuo-ji', + source: ['/book/:id/*'], + target: '/creative-comic/:id', + }, + ], + }, +}; diff --git a/lib/v2/creative-comic/router.js b/lib/v2/creative-comic/router.js new file mode 100644 index 000000000..23ab64e76 --- /dev/null +++ b/lib/v2/creative-comic/router.js @@ -0,0 +1,3 @@ +module.exports = (router) => { + router.get('/book/:id/:coverOnly?/:quality?', require('./book')); +}; diff --git a/lib/v2/creative-comic/templates/chapter.art b/lib/v2/creative-comic/templates/chapter.art new file mode 100644 index 000000000..d65dacfbd --- /dev/null +++ b/lib/v2/creative-comic/templates/chapter.art @@ -0,0 +1,11 @@ +{{ if chapter }} +

{{ chapter.name }}

+{{ /if }} +{{ if pages }} + {{ each pages page }} +
+ {{ /each }} +{{ /if }} +{{ if cover }} + +{{ /if }} diff --git a/lib/v2/creative-comic/utils.js b/lib/v2/creative-comic/utils.js new file mode 100644 index 000000000..6bed527c2 --- /dev/null +++ b/lib/v2/creative-comic/utils.js @@ -0,0 +1,96 @@ +const got = require('@/utils/got'); +const CryptoJS = require('crypto-js'); + +const apiHost = 'https://api.creative-comic.tw'; +const siteHost = 'https://www.creative-comic.tw'; +const device = 'web_desktop'; +const DEFAULT_TOKEN = 'freeforccc2020reading'; + +const getBook = (bookId, uuid) => + got(`${apiHost}/book/${bookId}/info`, { + headers: { + device, + uuid, + }, + }); + +const getChapter = (id, uuid) => + got(`${apiHost}/book/chapter/${id}`, { + headers: { + device, + uuid, + }, + }); + +const getChapters = (bookId, uuid) => + got(`${apiHost}/book/${bookId}/chapter`, { + headers: { + device, + uuid, + }, + }); + +const getImgEncrypted = async (pageId, quality) => { + const { data: res } = await got(`${siteHost}/fs/chapter_content/encrypt/${pageId}/${quality}`, { + headers: { + device, + }, + responseType: 'buffer', + }); + return Buffer.from(res).toString('base64'); +}; + +const getImgKey = (pageId, uuid) => + got(`${apiHost}/book/chapter/image/${pageId}`, { + headers: { + device, + uuid, + }, + }); + +const getUuid = (tryGet) => + tryGet('creative-comic:uuid', async () => { + const { data } = await got(`${apiHost}/guest`, { + headers: { + device, + }, + }); + return data.data; + }); + +const decrypt = (encrypted, secrets) => + CryptoJS.AES.decrypt(encrypted, CryptoJS.enc.Hex.parse(secrets.key), { + iv: CryptoJS.enc.Hex.parse(secrets.iv), + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7, + }).toString(CryptoJS.enc.Utf8); + +const token2Key = (token) => { + const t = CryptoJS.SHA512(token).toString(); + return { + key: t.substring(0, 64), + iv: t.substring(30, 62), // t.substr(30, 32) + }; +}; + +const getRealKey = (imgKey, token = DEFAULT_TOKEN) => { + const secrets = token2Key(token); + const key = decrypt(imgKey, secrets); + const realKey = key.split(':'); + return { + key: realKey[0], + iv: realKey[1], + }; +}; + +module.exports = { + getBook, + getChapter, + getChapters, + getImgEncrypted, + getImgKey, + getUuid, + decrypt, + token2Key, + getRealKey, +};