diff --git a/.github/workflows/task.yml b/.github/workflows/task.yml index 336f0d6..2961378 100644 --- a/.github/workflows/task.yml +++ b/.github/workflows/task.yml @@ -13,7 +13,7 @@ on: - favorites - leaderboard PICA_DL_SEARCH_KEYWORDS: - description: '搜索关键字,多个用 # 隔开' + description: '搜索关键字或者漫画ID (多个用 # 隔开)' type: string OUTPUT_ZIP: description: '每个章节打成独立压缩包' diff --git a/package.json b/package.json index 3b0c1b7..49a952b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pica-cli", "author": "justorez", - "version": "1.2.0", + "version": "1.3.0", "description": "😉 哔咔漫画下载器", "packageManager": "pnpm@8.14.3", "type": "module", @@ -17,7 +17,7 @@ "start:zip": "node dist/zip.js", "build": "rimraf dist && rollup --config rollup.config.js", "test": "vitest", - "pub": "pnpm build && pnpm publish", + "pub": "pnpm build && npm publish", "check": "tsc --incremental --noEmit", "lint": "eslint --cache --ext js,cjs,ts, --ignore-path .gitignore .", "lint:fix": "eslint --cache --fix --ignore-path .gitignore .", @@ -86,6 +86,7 @@ "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", + "figures": "^6.0.1", "lint-staged": "^15.2.0", "mime": "^4.0.1", "ora": "^8.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b09b908..c3578cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -76,6 +76,9 @@ devDependencies: eslint-plugin-prettier: specifier: ^5.1.3 version: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.56.0)(prettier@3.2.4) + figures: + specifier: ^6.0.1 + version: 6.0.1 lint-staged: specifier: ^15.2.0 version: 15.2.0(supports-color@9.4.0) @@ -1848,6 +1851,13 @@ packages: escape-string-regexp: 1.0.5 dev: true + /figures@6.0.1: + resolution: {integrity: sha512-0oY/olScYD4IhQ8u//gCPA4F3mlTn2dacYmiDm/mbDQvpmLjV4uH+zhsQ5IyXRyvqkvtUkXkNdGvg5OFJTCsuQ==} + engines: {node: '>=18'} + dependencies: + is-unicode-supported: 2.0.0 + dev: true + /file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} diff --git a/readme.md b/readme.md index e9d93bf..ae0e203 100644 --- a/readme.md +++ b/readme.md @@ -4,18 +4,16 @@ 😉 哔咔漫画下载器 -![演示](https://s2.loli.net/2024/01/29/rhcOo4GBD8kLEqv.gif) +![演示](https://s2.loli.net/2024/02/01/Qc7L3qGZOWBPmkR.gif) - 交互式命令行 - 排行榜:下载当前排行榜的全部漫画 - 收藏夹:下载当前用户收藏夹的全部漫画 -- 关键词搜索:支持多选 +- 搜索:支持关键字和漫画ID (多个用 # 隔开)。访问哔咔电脑端网站,进入漫画详情,地址栏链接里的 `cid` 就是漫画ID - 自动过滤已下载的章节和图片,不会重复下载 - 如果没有相关环境变量,则启动交互命令界面;若有则直接执行 -- 通过 `pica-zip` 命令分章节批量压缩,配合支持 zip 包的漫画阅读软件使用,比如 [Perfect Viewer](https://play.google.com/store/apps/details?id=com.rookiestudio.perfectviewer)。
- 不限于 `pica-cli` 下载的漫画使用,只要符合 [cmoics/漫画标题/漫画章节/漫画图片](#) 的目录结构即可。 -- 借助 github action 实现飞速下载,支持从 github artifact 和 file.io(无需注册和科学上网)两种方式下载完整漫画包。
- file.io 文件保存两周,单文件最大 2GB,注意链接只能下载**一次**,下载后文件会自动删除 +- 通过 `pica-zip` 命令分章节批量压缩,配合支持 zip 的漫画阅读软件使用,比如 [Perfect Viewer](https://play.google.com/store/apps/details?id=com.rookiestudio.perfectviewer)。不限于 `pica-cli` 下载的漫画,只要符合 [cmoics/漫画标题/漫画章节/漫画图片](#) 的目录结构即可。 +- 借助 github action 实现飞速下载,支持从 github artifact 和 file.io 两种方式下载完整漫画包。file.io 无需注册,无需科学上网,文件保存两周,单文件最大 2GB,注意链接只能下载**一次**,下载后文件会自动删除 如果用的开心,求个 star 支持一下,比心 ~ ❤️ @@ -35,11 +33,11 @@ PICA_ACCOUNT PICA_PASSWORD ``` -fork 一份本仓库,将上面三个环境变量,设置为仓库密钥: +fork 一份[本仓库](https://github.com/justorez/pica-cli),将上面三个环境变量,设置为仓库密钥: ![action secret](https://s2.loli.net/2024/01/30/5FxU7olyWC3VAe1.png) -然后点击 Actions,再点击左侧的 `task` 工作流,再点击右侧的 `Run workflow`,输入相关的信息,点击运行即可。 +然后点击 Actions,再点击左侧的 `task` 工作流,再点击右侧的 `Run workflow`,输入相关的信息,点击运行。 ![action run](https://s2.loli.net/2024/01/30/PmfublZKLFQrth9.png) @@ -51,7 +49,7 @@ fork 一份本仓库,将上面三个环境变量,设置为仓库密钥: ![file.io](https://s2.loli.net/2024/01/31/UT4i6zpGjYvDxL3.png) -如果你想自定义过程,请自行修改 [.github/workflows/task.yaml](.github/workflows/task.yaml)。 +如果你想自定义过程,请自行修改 [.github/workflows/task.yml](.github/workflows/task.yml)。 ### 方式二:直接安装 @@ -110,6 +108,7 @@ pnpm dev:zip ## 更新日志 +- 2024/02/01 支持通过漫画ID精确下载 - 2024/01/31 github action 同时将漫画包上传到 file.io - 2024/01/30 提供 github action 的下载方式 - 2024/01/29 下载完成后,提供命令把漫画按章节批量压缩 diff --git a/resource/1-1.gif b/resource/1-1.gif new file mode 100644 index 0000000..66dfc39 Binary files /dev/null and b/resource/1-1.gif differ diff --git a/resource/1.gif b/resource/1.gif deleted file mode 100644 index cb67631..0000000 Binary files a/resource/1.gif and /dev/null differ diff --git a/resource/2-6.png b/resource/2-6.png new file mode 100644 index 0000000..0dbc464 Binary files /dev/null and b/resource/2-6.png differ diff --git a/scripts/upload.js b/scripts/upload.js index 6e2aad4..e96c5bb 100644 --- a/scripts/upload.js +++ b/scripts/upload.js @@ -6,10 +6,20 @@ import axios from 'axios' import mime from 'mime' import AdmZip from 'adm-zip' import pico from 'picocolors' +import figures from 'figures' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const MAX_SIZE = 2 * 1024 * 1024 * 1024 // 2GB +const log = { + log: (...msg) => console.log(...msg), + info: (...msg) => console.log(pico.cyan('➡️'), ...msg), + warn: (...msg) => + console.log(pico.yellow(`${figures.warning} ${msg.join(' ')}`)), + error: (...msg) => + console.log(pico.red(`${figures.cross} ${msg.join(' ')}`)) +} + // https://file.io/ async function main() { let root = path.resolve(__dirname, '../comics-zip') @@ -17,7 +27,7 @@ async function main() { if (!existsSync(root)) { root = path.resolve(__dirname, '../comics') if (!existsSync(root)) { - console.log(pico.yellow('没有发现已下载的漫画')) + log.warn('没有发现已下载的漫画') return } } @@ -41,11 +51,11 @@ async function main() { `https://file.io?title=${filename}`, form ) - console.log( + log.log( `${pico.cyan(filename)} 已上传到 file.io. 下载地址:${pico.green(data.link)}` ) } else { - console.log(pico.yellow(`${filename} 大小超过了 2GB`)) + log.warn(`${filename} 大小超过了 2GB`) } } catch (error) { console.error(error) diff --git a/src/index.ts b/src/index.ts index 3ac35eb..240e1d0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,18 @@ import { Pica } from './sdk' -import { filterEpisodes, filterPictures, loadEnv, mark } from './utils' +import { + log, + mark, + loadEnv, + filterEpisodes, + filterPictures, + isValidComicId +} from './utils' import ora from 'ora' import { select, checkbox, input } from '@inquirer/prompts' import ProgressBar from 'progress' import { Comic } from './types' import pLimit from 'p-limit' -import picos from 'picocolors' +import pico from 'picocolors' loadEnv() @@ -42,25 +49,42 @@ async function main() { if (answer === 'search') { if (PICA_IS_GITHUB && !PICA_DL_SEARCH_KEYWORDS) { - console.log(picos.yellow('没有输入搜索关键字')) + log.warn('没有输入搜索关键字') return } let searchRes: Comic[] = [] - const keywords = + const inputStr = PICA_DL_SEARCH_KEYWORDS || (await input({ - message: '请输入关键字(多个用 # 隔开)', + message: '请输入关键字或者漫画ID (多个用 # 隔开)', transformer: (val) => val.trim() })) - if (!keywords) { - console.log(picos.yellow('没有输入搜索关键字')) + if (!inputStr) { + log.warn('没有输入搜索关键字') return } - for (const keyword of keywords.split('#')) { + const inputKeys = inputStr.split('#') + + // 根据漫画ID查询 + const bookIds = inputKeys.filter((k: string) => isValidComicId(k)) + for (const id of bookIds) { + try { + const info = await pica.comicInfo(id) + info.title = info.title.trim() + comics.push(info) + log.info(`${info.title} 已加入下载队列`) + } catch (error) { + log.error(`无效漫画ID ${id}`) + } + } + + // 根据关键字查询 + const keywords = inputKeys.filter((k: string) => !isValidComicId(k)) + for (const keyword of keywords) { spinner.start(`正在搜索 ${keyword}`) searchRes = await pica.searchAll(keyword) spinner.stop() @@ -91,9 +115,7 @@ async function main() { episodes = filterEpisodes(episodes, cid) spinner.stop() - console.log( - `${picos.cyan('➡️')} ${title} 查询到 ${episodes.length} 个章节` - ) + log.info(`${title} 查询到 ${episodes.length} 个章节`) for (const ep of episodes) { spinner.start(`正在获取章节 ${ep.title} 的图片信息`) @@ -102,7 +124,7 @@ async function main() { spinner.stop() const bar = new ProgressBar( - `${picos.cyan('➡️')} ${title} ${ep.title} [:bar] :current/:total`, + `${pico.cyan('➡️')} ${title} ${ep.title} [:bar] :current/:total`, { incomplete: ' ', width: 20, @@ -129,12 +151,13 @@ async function main() { mark(cid, ep.id) } - console.log(picos.green(`✓ ${picos.bold(title)} 下载完成`)) + log.success(`${title} 下载完成`) } } process.on('uncaughtException', (err) => { - console.log(`\n${picos.red(err.message)}`) + console.log('\n') + log.error(`${err.message}`) process.exit(0) }) diff --git a/src/sdk.ts b/src/sdk.ts index 697a008..b87a71d 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -148,7 +148,7 @@ export class Pica { */ async comicInfo(bookId: string) { const url = `comics/${bookId}` - const res = await this.request('get', url) + const res = await this.request('get', url) return res.comic } diff --git a/src/utils.ts b/src/utils.ts index 6d422f0..c3bdaf0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,9 +3,15 @@ import dotenv from 'dotenv' import fs from 'node:fs' import { Episode, Picture } from './types' import Debug from 'debug' +import pico from 'picocolors' +import figures from 'figures' export const debug = Debug('pica') +export function isValidComicId(cid: string) { + return /^[0-9a-zA-Z]{24}$/.test(cid) +} + /** * 标记某章节已下载完成,并记录到本地临时文件 */ @@ -81,3 +87,20 @@ export function normalizeName(s: string) { .replace(/>/g, '>') .replace(/:/g, '-') } + +type LogMsg = string[] | number[] +// √ ✕ +export const log = { + log: (...msg: LogMsg) => console.log(...msg), + info: (...msg: LogMsg) => console.log(pico.cyan('➡️'), ...msg), + warn: (...msg: LogMsg) => + console.log(pico.yellow(`${figures.warning} ${msg.join(' ')}`)), + error: (...msg: LogMsg) => + console.log(pico.red(`${figures.cross} ${msg.join(' ')}`)), + success: (...msg: LogMsg) => + console.log(pico.green(`${figures.tick} ${msg.join(' ')}`)) +} + +export function sleep(s: number) { + return new Promise((r) => setTimeout(r, s * 1000)) +} diff --git a/src/zip.ts b/src/zip.ts index d1e45b4..05d522d 100644 --- a/src/zip.ts +++ b/src/zip.ts @@ -1,15 +1,16 @@ import AdmZip from 'adm-zip' import path from 'node:path' import fs from 'node:fs' -import pico from 'picocolors' import ProgressBar from 'progress' +import pico from 'picocolors' +import { log } from './utils' function main() { const root = path.resolve(process.cwd(), 'comics') const dest = path.resolve(process.cwd(), 'comics-zip') if (!fs.existsSync(root)) { - console.log(pico.yellow('没有发现已下载的漫画')) + log.warn('没有发现已下载的漫画') return } @@ -18,11 +19,11 @@ function main() { }) if (comics.length === 0) { - console.log(pico.yellow('没有发现已下载的漫画')) + log.warn('没有发现已下载的漫画') return } - console.log( + log.info( `${pico.cyan(comics.length)} 本漫画等待打包:${pico.cyan(comics.join(', '))}` ) @@ -50,8 +51,10 @@ function main() { bar.tick() } - console.log(pico.green(`✓ ${comic} 打包完成`)) + // log.success(`${comic} 打包完成`) } + + log.success(`打包完成`) } main()