diff --git a/README.md b/README.md index 17629f2b5..f7bb3ca2d 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ jmcomic.download_album(123, option) ### 3. 使用命令行 > 如果只想下载本子,使用命令行会比上述方式更加简单直接 > -> 例如,在windows上,直接按下win+r键,输入jmcomic xxx就可以下载本子。 +> 例如,在windows上,直接按下 win+R 键,输入`jmcomic xxx`就可以下载本子。 示例: @@ -147,11 +147,67 @@ b. 配置环境变量 `JM_OPTION_PATH` 为option文件路径(推荐) jmcomic 123 ``` +### 4. 查看本子详情(jmv 命令) + +> `jmv` 命令用于快速查看本子详情,不做下载。 +> +> **适用场景**:在某些网站上看到一串*神秘车号*,想快速看看具体是啥本子。此时只需copy原文本,按下 win+R,输入`jmv [粘贴内容]`即可 +> +> 支持从任意文本中提取数字作为车号,方便直接粘贴各种格式的车号。 + +示例: + +```sh +# 直接输入车号 +jmv 350234 + +# 从混合文本中提取数字(提取出 350234) +jmv 350谁还没看过234 + +# 指定option文件(也支持环境变量,用法同上) +jmv 350234 --option="D:/a.yml" + +# -y 参数:执行完毕后直接退出,无需按回车确认 +jmv 350234 -y +``` + +输出效果: + +```text +🔍 正在查询 禁漫车号 - [350234] 的详情... + +────────────────────────────────────────────────── + 📖 标题: xxx + 🆔 ID: JM350234 + 🔗 链接: https://18comic.vip/album/350234/ + ✍️ 作者: Author1, Author2 +────────────────────────────────────────────────── + 📅 发布日期: 2022-06-15 + 📅 更新日期: 2023-01-01 + 📄 总页数: 50 + 👀 观看: 2M + ❤️ 点赞: 77K + 💬 评论: 9801 +────────────────────────────────────────────────── + 🏷️ 标签: 标签1, 标签2, ... + 🎭 人物: 角色A, 角色B, ... + 📚 作品: 作品1, 作品2, ... +────────────────────────────────────────────────── + 📑 章节 (2): + 第1話 上 (id: 350234) + 第2話 下 (id: 350235) +────────────────────────────────────────────────── + +[运行结束] 请按回车键关闭窗口... (下次运行可附加 -y 参数跳过确认) +``` + ## 进阶使用 -请查阅文档首页→[jmcomic.readthedocs.io](https://jmcomic.readthedocs.io/zh-cn/latest) +请查阅文档首页 → [jmcomic.readthedocs.io](https://jmcomic.readthedocs.io/zh-cn/latest) + +或者查看github仓库的文档 → [github-repo-docs](https://github.com/hect0x7/JMComic-Crawler-Python/blob/master/assets/docs/sources/tutorial/0_common_usage.md) (提示:jmcomic提供了很多下载配置项,大部分的下载需求你都可以尝试寻找相关配置项或插件来实现。) diff --git a/assets/docs/sources/tutorial/2_command_line.md b/assets/docs/sources/tutorial/2_command_line.md index a65fac3d7..c201b2d6b 100644 --- a/assets/docs/sources/tutorial/2_command_line.md +++ b/assets/docs/sources/tutorial/2_command_line.md @@ -1,22 +1,24 @@ # 命令行教程 -## 1. 基本用法 +## 1. jmcomic - 下载本子 -``` +### 1.1 基本用法 + +```sh # 下载album 123 456,下载photo 333。彼此之间使用空格间隔 jmcomic 123 456 p333 ``` -## 2. 自定义option +### 1.2 自定义option -### 2.1. 通过命令行 +#### 通过命令行参数 使用 --option 参数指定option配置文件路径 ```sh jmcomic 123 --option="D:/a.yml" ``` -### 2.2. 使用环境变量 +#### 使用环境变量 配置环境变量 `JM_OPTION_PATH` 为option配置文件路径 > 请自行google配置环境变量的方式,或使用powershell命令: `setx JM_OPTION_PATH "D:/a.yml"` 重启后生效 @@ -24,3 +26,66 @@ jmcomic 123 --option="D:/a.yml" ```sh jmcomic 123 ``` + +## 2. jmv - 查看本子详情 + +`jmv` 命令用于快速查看本子详情,无需下载。支持从任意文本中提取数字作为车号。 + +### 2.1 基本用法 + +```sh +# 直接输入车号 +jmv 350234 + +# 从混合文本中提取数字(提取出 350234) +jmv 350谁还没看过234 + +# JM前缀也可以 +jmv JM350234 +``` + +### 2.2 附加参数 + +与 `jmcomic` 命令类似,支持 `--option` 参数和 `JM_OPTION_PATH` 环境变量: + +```sh +jmv 350234 --option="D:/a.yml" +``` + +如果希望在执行完毕后立刻退出且不出现“请按回车键关闭窗口...”的停留提示(适用于自动化代码调用或常规终端使用时),可添加 `-y` 选项: + +```sh +jmv 350234 -y +``` + +### 2.3 输出示例 + +```text +🔍 正在查询 禁漫车号 - [350234] 的详情... + +────────────────────────────────────────────────── + 📖 标题: xxx + 🆔 ID: JM350234 + 🔗 链接: https://18comic.vip/album/350234/ + ✍️ 作者: Author1, Author2 +────────────────────────────────────────────────── + 📅 发布日期: 2022-06-15 + 📅 更新日期: 2023-01-01 + 📄 总页数: 50 + 👀 观看: 2M + ❤️ 点赞: 77K + 💬 评论: 9801 +────────────────────────────────────────────────── + 🏷️ 标签: 标签1, 标签2, ... + 🎭 人物: 角色A, 角色B, ... + 📚 作品: 作品1, 作品2, ... +────────────────────────────────────────────────── + 📑 章节 (2): + 第1話 上 (id: 350234) + 第2話 下 (id: 350235) +────────────────────────────────────────────────── + +[运行结束] 请按回车键关闭窗口... (下次运行可附加 -y 参数跳过确认) +``` + +> **说明**: 当作者、标签、人物、作品超过10个时,会自动截断并显示总数(如 `...等25个`)。 diff --git a/setup.py b/setup.py index 5ffbff495..75b624bfc 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,8 @@ ], entry_points={ 'console_scripts': [ - 'jmcomic = jmcomic.cl:main' + 'jmcomic = jmcomic.cl:main', + 'jmv = jmcomic.cl:view_main', ] } ) diff --git a/src/jmcomic/__init__.py b/src/jmcomic/__init__.py index 10c7d0205..4c2fce5e4 100644 --- a/src/jmcomic/__init__.py +++ b/src/jmcomic/__init__.py @@ -2,7 +2,7 @@ # 被依赖方 <--- 使用方 # config <--- entity <--- toolkit <--- client <--- option <--- downloader -__version__ = '2.6.15' +__version__ = '2.6.16' from .api import * from .jm_plugin import * diff --git a/src/jmcomic/cl.py b/src/jmcomic/cl.py index a3e2add36..0e9aec6e4 100644 --- a/src/jmcomic/cl.py +++ b/src/jmcomic/cl.py @@ -1,10 +1,15 @@ """ command-line usage -for example, download album 123 456, photo 333: +1. jmcomic - download album/photo: -$ jmcomic 123 456 p333 --option="D:/option.yml" + $ jmcomic 123 456 p333 --option="D:/option.yml" +2. jmv - view album detail (extract digits from text as album id): + + $ jmv 350234 + $ jmv 350谁还没看过234 + $ jmv abc123141 --option="D:/option.yml" """ import os.path @@ -119,3 +124,133 @@ def run(self, option): def main(): JmcomicUI().main() + + +class JmViewUI: + + def __init__(self) -> None: + self.raw_text: str = '' + self.option_path: Optional[str] = None + self.auto_exit: bool = False + + def parse_arg(self): + import argparse + parser = argparse.ArgumentParser( + prog='jmv', + description='JMComic Album Viewer - 从文本中提取数字作为album ID,查看本子详情', + ) + parser.add_argument( + 'text', + help='包含数字的禁漫车号,例如 "350谁还没看过234",会提取出 "350234" 作为 album ID', + ) + parser.add_argument( + '--option', + help='option 文件路径,也可通过环境变量 JM_OPTION_PATH 指定', + type=str, + default=get_env('JM_OPTION_PATH', ''), + ) + parser.add_argument( + '-y', '--yes', + action='store_true', + help='执行完毕后直接退出,无需按回车确认', + ) + + args = parser.parse_args() + self.raw_text = args.text + self.auto_exit = args.yes + + option_str = args.option + if len(option_str) == 0 or option_str == "''": + self.option_path = None + else: + self.option_path = os.path.abspath(option_str) + + def extract_album_id(self) -> str: + import re + numbers = re.findall(r'\d+', self.raw_text) + if not numbers: + from .api import jm_log + jm_log('jmv', f'❌❌❌ 解析失败: 无法从 "{self.raw_text}" 中提取到任何数字 ❌❌❌') + exit(1) + album_id = ''.join(numbers) + return album_id + + @staticmethod + def _truncate_list(items, limit=10): + if len(items) <= limit: + return ', '.join(items) + return ', '.join(items[:limit]) + f' ...等{len(items)}个' + + def print_album_detail(self, album): + from jmcomic import JmcomicText + + sep = '─' * 50 + + print(f'\n{sep}') + print(f' 📖 标题: {album.name}') + print(f' 🆔 ID: JM{album.album_id}') + print(f' 🔗 链接: {JmcomicText.format_album_url(album.album_id)}') + print(f' 🎨 封面: {JmcomicText.get_album_cover_url(album.album_id)}') + print(f' ✍️ 作者: {self._truncate_list(album.authors) if album.authors else "未知"}') + print(sep) + + print(f' 📅 发布日期: {album.pub_date}') + print(f' 📅 更新日期: {album.update_date}') + print(f' 📄 总页数: {album.page_count}') + print(f' 👀 观看: {album.views}') + print(f' ❤️ 点赞: {album.likes}') + print(f' 💬 评论: {album.comment_count}') + print(sep) + + if album.tags: + print(f' 🏷️ 标签: {self._truncate_list(album.tags)}') + if album.actors: + print(f' 🎭 人物: {self._truncate_list(album.actors)}') + if album.works: + print(f' 📚 作品: {self._truncate_list(album.works)}') + + if album.description: + print(f' 📝 简介: {album.description}') + + print(sep) + episode_count = len(album.episode_list) + print(f' 📑 章节 ({episode_count}):') + for pid, pindex, pname in album.episode_list: + pname = pname.strip() + print(f' 第{pindex}話 {pname} (id: {pid})') + + print(f'{sep}\n') + + def _pause(self): + if not self.auto_exit: + input('\n[运行结束] 请按回车键关闭窗口... (下次运行可附加 -y 参数跳过确认)') + + def main(self): + self.parse_arg() + + import atexit + atexit.register(self._pause) + + album_id = self.extract_album_id() + + from .api import jm_log + jm_log('jmv', f'🔍 正在查询 禁漫车号 - [{album_id}] 的详情...') + + from .api import create_option, JmOption + if self.option_path is not None: + option = create_option(self.option_path) + else: + option = JmOption.default() + + client = option.new_jm_client() + try: + album = client.get_album_detail(album_id) + except Exception as e: + jm_log('jmv', f'❌❌❌ 获取失败: album {album_id} 详情请求出错, 原因: {e}', e) + exit(1) + + self.print_album_detail(album) + + +def view_main(): + JmViewUI().main() diff --git a/src/jmcomic/jm_toolkit.py b/src/jmcomic/jm_toolkit.py index c859fdfa8..72434f372 100644 --- a/src/jmcomic/jm_toolkit.py +++ b/src/jmcomic/jm_toolkit.py @@ -431,6 +431,7 @@ def compare_versions(cls, v1: str, v2: str) -> int: else: return 0 # 相等 + # 支持dsl: #{???} -> os.getenv(???) JmcomicText.dsl_replacer.add_dsl_and_replacer(r'\$\{(.*?)\}', JmcomicText.match_os_env) diff --git a/tests/test_jmcomic/test_jm_cli.py b/tests/test_jmcomic/test_jm_cli.py new file mode 100644 index 000000000..8c2a6b7a1 --- /dev/null +++ b/tests/test_jmcomic/test_jm_cli.py @@ -0,0 +1,134 @@ +from test_jmcomic import * +from io import StringIO +from unittest.mock import patch + +from jmcomic.cl import JmcomicUI, JmViewUI + + +class Test_Cli(JmTestConfigurable): + """测试 CLI 命令 (jmcomic + jmv)""" + + album_id = '350234' + + # ========== jmcomic 命令测试 ========== + + def test_jmcomic_parse_album_id(self): + """jmcomic 解析 album id""" + ui = JmcomicUI() + ui.raw_id_list = [self.album_id] + ui.parse_raw_id() + self.assertEqual(ui.album_id_list, [self.album_id]) + + def test_jmcomic_parse_photo_id(self): + """jmcomic 解析 photo id (p前缀)""" + ui = JmcomicUI() + ui.raw_id_list = [f'p{self.album_id}'] + ui.parse_raw_id() + self.assertEqual(ui.photo_id_list, [self.album_id]) + + def test_jmcomic_parse_mixed(self): + """jmcomic 同时解析 album 和 photo""" + ui = JmcomicUI() + ui.raw_id_list = [self.album_id, f'p{self.album_id}'] + ui.parse_raw_id() + self.assertEqual(ui.album_id_list, [self.album_id]) + self.assertEqual(ui.photo_id_list, [self.album_id]) + + def test_jmcomic_download_album(self): + """jmcomic 真实下载 album 350234""" + JustDownloadSpecificCountImage.count = 5 + album, _dler = download_album(self.album_id, self.option, downloader=JustDownloadSpecificCountImage) + self.assertEqual(album.album_id, self.album_id) + self.assertTrue(len(album.name) > 0, '标题不应为空') + + # ========== jmv 命令测试 ========== + + # -- extract_album_id -- + + def test_jmv_extract_pure_digits(self): + """jmv 纯数字文本""" + ui = JmViewUI() + ui.raw_text = '350234' + self.assertEqual(ui.extract_album_id(), '350234') + + def test_jmv_extract_digits_scattered(self): + """jmv 数字散布在文本中,应拼接所有数字""" + ui = JmViewUI() + ui.raw_text = '350谁还没看过234' + self.assertEqual(ui.extract_album_id(), '350234') + + def test_jmv_extract_jm_prefix(self): + """jmv JM前缀的车号""" + ui = JmViewUI() + ui.raw_text = 'JM350234' + self.assertEqual(ui.extract_album_id(), '350234') + + def test_jmv_extract_no_digits_exits(self): + """jmv 没有数字时应 exit(1)""" + ui = JmViewUI() + ui.raw_text = 'abcdef' + with self.assertRaises(SystemExit) as ctx: + ui.extract_album_id() + self.assertEqual(ctx.exception.code, 1) + + # -- _truncate_list -- + + def test_jmv_truncate_list_under_limit(self): + """不超过限制时完整显示""" + items = ['a', 'b', 'c'] + result = JmViewUI._truncate_list(items, limit=10) + self.assertEqual(result, 'a, b, c') + + def test_jmv_truncate_list_at_limit(self): + """刚好等于限制时完整显示""" + items = [str(i) for i in range(10)] + result = JmViewUI._truncate_list(items, limit=10) + self.assertNotIn('...', result) + + def test_jmv_truncate_list_over_limit(self): + """超过限制时截断并显示总数""" + items = [str(i) for i in range(20)] + result = JmViewUI._truncate_list(items, limit=10) + self.assertIn('...等20个', result) + + # -- 真实网络请求 -- + + def test_jmv_get_album_detail_real(self): + """jmv 真实请求 album 350234 的详情""" + album = self.client.get_album_detail(self.album_id) + + self.assertEqual(album.album_id, self.album_id) + self.assertTrue(len(album.name) > 0, '标题不应为空') + self.assertTrue(len(album.episode_list) > 0, '章节列表不应为空') + + def test_jmv_print_album_detail_real(self): + """jmv 真实请求并打印 album 350234 的详情,校验输出内容""" + album = self.client.get_album_detail(self.album_id) + ui = JmViewUI() + + with patch('sys.stdout', new_callable=StringIO) as mock_out: + ui.print_album_detail(album) + output = mock_out.getvalue() + + # 校验基本信息 + self.assertIn(f'JM{self.album_id}', output) + self.assertIn(album.name, output) + self.assertIn(JmcomicText.format_album_url(album.album_id), output) + + # 校验章节 + self.assertIn(f'章节 ({len(album.episode_list)})', output) + + def test_jmv_print_truncates_real(self): + """jmv 真实请求 album 350234,验证超长列表被截断""" + album = self.client.get_album_detail(self.album_id) + ui = JmViewUI() + + with patch('sys.stdout', new_callable=StringIO) as mock_out: + ui.print_album_detail(album) + output = mock_out.getvalue() + + # 350234 的作者/标签数量非常多,应触发截断 + if len(album.authors) > 10: + self.assertIn(f'...等{len(album.authors)}个', output) + if len(album.tags) > 10: + self.assertIn(f'...等{len(album.tags)}个', output)