v2.5.7: 跟进禁漫v1.6.7版本更新的分类参数,优化代码和文档。

This commit is contained in:
hect0x7 2024-03-04 22:54:27 +08:00 committed by GitHub
parent fbaaffc464
commit 147c796c5d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 348 additions and 188 deletions

View File

@ -14,16 +14,26 @@
本项目的核心功能是下载本子,基于此,设计了一套方便使用、便于扩展,能满足一些特殊下载需求的框架。
除了下载功能以外,也实现了其他的一些禁漫接口,例如登录、搜索、收藏夹、分类、排行榜等等,按需实现
目前核心功能实现较为稳定,项目也处于维护阶段
目前核心功能实现较为稳定,项目也处于维护阶段(因为禁漫接口经常变动,需要经常维护)。
除了下载功能以外,也实现了其他的一些禁漫接口,按需实现,具体如下。
### 已实现的禁漫API
- 登录
- 搜本
- 分类 (排行榜)
- 本子章节详情
- 图片下载解码
- 收藏夹
- 移动端接口加解密
## 安装教程
* 通过pip官方源安装推荐并且更新也是这个命令
```shell
pip install jmcomic -i https://pypi.org/project --upgrade
pip install jmcomic -i https://pypi.org/project -U
```
* 通过源代码安装
@ -47,6 +57,21 @@ jmcomic.download_album('422866') # 传入要下载的album的id即可下载
$ jmcomic 422866
```
## 进阶使用
文档网站:[jmcomic.readthedocs.io](https://jmcomic.readthedocs.io/en/latest)
进阶使用可以参考:[jmcomic常用类和方法演示](assets/docs/sources/tutorial/0_demo.md)
下面列出的是一些常用的文档:
* [jmcomic常用类和方法演示](assets/docs/sources/tutorial/0_demo.md)
* [option配置文件语法包含插件配置](./assets/docs/sources/option_file_syntax.md)
* [GitHub Actions使用教程](./assets/docs/sources/tutorial/1_github_actions.md)
* [命令行使用教程](assets/docs/sources/tutorial/2_command_line.md)
* [插件机制](assets/docs/sources/tutorial/6_plugin.md)
* [下载过滤器机制](assets/docs/sources/tutorial/5_filter.md)
## 项目特点
- **绕过Cloudflare的反爬虫**
@ -83,19 +108,6 @@ $ jmcomic 422866
- `jpg图片合成为一个pdf插件`
- `导出收藏夹为csv文件插件`
## 进阶使用
进阶使用请查阅文档:[文档](https://jmcomic.readthedocs.io/en/latest)
下面列出一些常用的文档链接:
* [option配置文件语法包含插件配置](./assets/docs/sources/option_file_syntax.md)
* [常用类和方法演示(下载本子、获取实体类、搜索本子)](assets/docs/sources/tutorial/3_demo.md)
* [命令行使用教程](assets/docs/sources/tutorial/2_command_line.md)
* [GitHub Actions使用教程](./assets/docs/sources/tutorial/1_github_actions.md)
* [插件机制](assets/docs/sources/tutorial/6_plugin.md)
* [下载过滤器机制](assets/docs/sources/tutorial/5_filter.md)
## 使用小说明
* Python >= 3.7
@ -103,10 +115,11 @@ $ jmcomic 422866
## 项目文件夹介绍
* .githubGitHub Actions配置文件
* assets存放一些非代码的资源文件
* config存放配置文件
* docs项目文档
* option存放配置文件
* src存放源代码

View File

@ -90,10 +90,13 @@ dir_rule:
# 本项只建议了解编程的朋友定制,实现在这个类: jmcomic.jm_option.DirRule
# 写法:
# 1. 以'Bd'开头,表示根目录
# 2. 文件夹每增加一层,使用'_'区隔
# 3. 文件夹名字表示为 Pxxx/Ayyy意思是 JmPhotoDetail.xxx / JmAlbumDetail的.yyy。xxx和yyy可以写什么需要看源码。
# 下面是示例,表示使用禁漫网站的默认下载方式 [根目录 / 本子id / 章节序号 / 图片文件]
# rule: Bd_Aid_Pindex
# 2. 文件夹每增加一层,使用 '_' 或者 '/' 区隔
# 3. 用Pxxx或者Ayyy指代文件夹名意思是 JmPhotoDetail.xxx / JmAlbumDetail的.yyy。xxx和yyy可以写什么需要看源码。
#
# 下面演示如果要使用禁漫网站的默认下载方式,该怎么写:
# 规则: 根目录 / 本子id / 章节序号 / 图片文件
# rule: 'Bd / Aid / Pindex'
# rule: 'Bd_Aid_Pindex'
# 默认规则是: 根目录 / 章节标题 / 图片文件
rule: Bd_Ptitle

View File

@ -0,0 +1,189 @@
# jmcomic 常用类和方法演示
## 下载本子/章节
```python
from jmcomic import *
# 下载id为438696的本子 (https://18comic.vip/album/438696)
download_album(438696)
# 下载章节 (https://18comic.vip/photo/438696)
download_photo(438696)
# 同时下载多个本子
download_album([123, 456, 789])
```
## 使用option定制化下载本子
如果你在下载本子时有一些定制化需求,
例如指定禁漫域名,使用代理,登录禁漫,图片格式转换等等,
那么你可以试试看jmcomic提供的option机制
```python
from jmcomic import *
# 1. 在调用下载api前通过创建和使用option对象可以定制化下载行为。
# 推荐使用配置文件的方式来创建option对象
# 你可以配置很多东西比如代理、cookies、下载规则等等。
# 配置文件的语法参考: https://jmcomic.readthedocs.io/en/latest/option_file_syntax/
option = create_option_by_file('op.yml') # 通过配置文件来创建option对象
# 2. 调用下载api把option作为参数传递
download_album(123, option)
# 也可以使用下面这种面向对象的方式,是一样的
option.download_album(123)
```
## 获取本子/章节/图片的实体类
```python
from jmcomic import *
# 客户端
client = JmOption.default().new_jm_client()
# 本子实体类
album: JmAlbumDetail = client.get_album_detail('427413')
def fetch(photo: JmPhotoDetail):
# 章节实体类
photo = client.get_photo_detail(photo.photo_id, False)
# 图片实体类
image: JmImageDetail
for image in photo:
print(image.img_url)
# 多线程发起请求
multi_thread_launcher(
iter_objs=album,
apply_each_obj_func=fetch
)
```
## 搜索本子
```python
from jmcomic import *
client = JmOption.default().new_jm_client()
# 分页查询search_site就是禁漫网页上的【站内搜索】
page: JmSearchPage = client.search_site(search_query='+MANA +无修正', page=1)
# page默认的迭代方式是page.iter_id_title(),每次迭代返回 albun_id, title
for album_id, title in page:
print(f'[{album_id}]: {title}')
# 直接搜索禁漫车号
page = client.search_site(search_query='427413')
album: JmAlbumDetail = page.single_album
print(album.tags)
```
## 搜索并下载本子
```python
from jmcomic import *
option = JmOption.default()
client = option.new_jm_client()
tag = '無修正'
# 搜索标签可以使用search_tag。
# 搜索第一页。
page: JmSearchPage = client.search_tag(tag, page=1)
aid_list = []
for aid, atitle, tag_list in page.iter_id_title_tag(): # 使用page的iter_id_title_tag迭代器
if tag in tag_list:
print(f'[标签/{tag}] 发现目标: [{aid}]: [{atitle}]')
aid_list.append(aid)
download_album(aid_list, option)
```
## 分类 / 排行榜
禁漫的分类是一个和搜索有些类似的功能。
搜索是按某一条件进行过滤。
分类没有过滤就是把某一类别category下的本子全都调出来。
禁漫的排行榜就是分类的一种形式
下面演示调用分类api
```python
from jmcomic import *
# 创建客户端
op = JmOption.default()
cl = op.new_jm_client()
# 调用分类接口
# 根据下面的参数,这个调用的意义就是:
# 在全部分类下,选择所有时间范围,按观看数排序后,获取第一页的本子
page: JmCategoryPage = cl.categories_filter(
page=1,
time=JmMagicConstants.TIME_ALL, # 时间选择全部具体可以写什么请见JmMagicConstants
category=JmMagicConstants.CATEGORY_ALL, # 分类选择全部具体可以写什么请见JmMagicConstants
order_by=JmMagicConstants.ORDER_BY_LATEST, # 按照观看数排序具体可以写什么请见JmMagicConstants
)
# 月排行底层实现也是调的categories_filter
page: JmCategoryPage = cl.month_ranking(1)
# 周排行
page: JmCategoryPage = cl.week_ranking(1)
# 循环获取分页,使用 cl.categories_filter_gen
for page in cl.categories_filter_gen(1, # 起始页码
# 下面是分类参数
JmMagicConstants.TIME_WEEK,
JmMagicConstants.CATEGORY_ALL,
JmMagicConstants.ORDER_BY_VIEW,
):
for aid, atitle in page:
print(aid, atitle)
```
## 手动创建Client
```python
# 默认的使用方式是先创建optionoption封装了所有配置然后由option.new_jm_client() 创建客户端client使用client可以访问禁漫接口
# 下面演示直接构造client的方式
from jmcomic import *
"""
创建JM客户端
:param postman: 负责实现HTTP请求的对象持有cookies、headers、proxies等信息
:param domain_list: 禁漫域名
:param retry_times: 重试次数
"""
# 网页端
cl = JmHtmlClient(
postman=JmModuleConfig.new_postman(),
domain_list=['18comic.vip'],
retry_times=1
)
# API端APP
cl = JmApiClient(
postman=JmModuleConfig.new_postman(),
domain_list=JmModuleConfig.DOMAIN_API_LIST,
retry_times=1
)
```

View File

@ -1,117 +0,0 @@
# 使用jmcomic实现简单功能
## 下载本子
```python
from jmcomic import *
ls = str_to_list('''
438696
https://18comic.vip/album/497896/
''')
download_album(ls)
```
## 获取实体类
```python
from jmcomic import *
# 客户端
client = JmOption.default().new_jm_client()
# 本子实体类
album: JmAlbumDetail = client.get_album_detail('427413')
def fetch(photo: JmPhotoDetail):
# 章节实体类
photo = client.get_photo_detail(photo.photo_id, False)
# 图片实体类
image: JmImageDetail
for image in photo:
print(image.img_url)
# 多线程发起请求
multi_thread_launcher(
iter_objs=album,
apply_each_obj_func=fetch
)
```
## 搜索本子
```python
from jmcomic import *
client = JmOption.default().new_jm_client()
# 分页查询search_site就是禁漫网页上的【站内搜索】
page: JmSearchPage = client.search_site(search_query='+MANA +无修正', page=1)
# page默认的迭代方式是page.iter_id_title(),每次迭代返回 albun_id, title
for album_id, title in page:
print(f'[{album_id}]: {title}')
# 直接搜索禁漫车号
page = client.search_site(search_query='427413')
album: JmAlbumDetail = page.single_album
print(album.tags)
```
## 搜索并下载本子
```python
from jmcomic import *
option = JmOption.default()
client = option.new_jm_client()
tag = '無修正'
# 搜索标签可以使用search_tag。
# 搜索第一页。
page: JmSearchPage = client.search_tag(tag, page=1)
aid_list = []
for aid, atitle, tag_list in page.iter_id_title_tag(): # 使用page的iter_id_title_tag迭代器
if tag in tag_list:
print(f'[标签/{tag}] 发现目标: [{aid}]: [{atitle}]')
aid_list.append(aid)
download_album(aid_list, option)
```
## 手动创建Client
```python
# 默认的使用方式是先创建optionoption封装了所有配置然后由option.new_jm_client() 创建客户端client使用client可以访问禁漫接口
# 下面演示直接构造client的方式
from jmcomic import *
"""
创建JM客户端
:param postman: 负责实现HTTP请求的对象持有cookies、headers、proxies等信息
:param domain_list: 禁漫域名
:param retry_times: 重试次数
"""
# 网页端
cl = JmHtmlClient(
postman=JmModuleConfig.new_postman(),
domain_list=['18comic.vip'],
retry_times=1
)
# API端APP
cl = JmApiClient(
postman=JmModuleConfig.new_postman(),
domain_list=JmModuleConfig.DOMAIN_API_LIST,
retry_times=1
)
```

View File

@ -67,3 +67,15 @@ for domain, status in domain_status_dict.items():
```
# 程序输出示例
```text
获取到7个域名开始测试
18comic.vip: ok
18comic.org: ok
18comic-palworld.vip: ok
18comic-c.art: ok
jmcomic1.me: ok
jmcomic.me: ok
18comic-palworld.club: ok
```

View File

@ -2,7 +2,7 @@
# 被依赖方 <--- 使用方
# config <--- entity <--- toolkit <--- client <--- option <--- downloader
__version__ = '2.5.6'
__version__ = '2.5.7'
from .api import *
from .jm_plugin import *

View File

@ -1,11 +1,12 @@
from .jm_downloader import *
__DOWNLOAD_API_RET = Tuple[JmAlbumDetail, JmDownloader]
def download_batch(download_api,
jm_id_iter: Union[Iterable, Generator],
option=None,
downloader=None,
) -> Set[Tuple[JmAlbumDetail, JmDownloader]]:
) -> Set[__DOWNLOAD_API_RET]:
"""
批量下载 album / photo
@ -46,7 +47,7 @@ def download_album(jm_album_id,
option=None,
downloader=None,
callback=None,
):
) -> Union[__DOWNLOAD_API_RET, Set[__DOWNLOAD_API_RET]]:
"""
下载一个本子album包含其所有的章节photo

View File

@ -6,8 +6,6 @@ Response Entity
"""
DictModel = AdvancedEasyAccessDict
class JmResp:
@ -87,11 +85,11 @@ class JmJsonResp(JmResp):
def json(self) -> Dict:
try:
return self.resp.json()
except Exception:
ExceptionTool.raises_resp('json解析失败', self, JsonResolveFailException)
except Exception as e:
ExceptionTool.raises_resp(f'json解析失败: {e}', self, JsonResolveFailException)
def model(self) -> DictModel:
return DictModel(self.json())
def model(self) -> AdvancedEasyAccessDict:
return AdvancedEasyAccessDict(self.json())
class JmApiResp(JmJsonResp):
@ -120,9 +118,9 @@ class JmApiResp(JmJsonResp):
return loads(self.decoded_data)
@property
def model_data(self) -> DictModel:
def model_data(self) -> AdvancedEasyAccessDict:
self.require_success()
return DictModel(self.res_data)
return AdvancedEasyAccessDict(self.res_data)
# album-comment

View File

@ -2,8 +2,8 @@ from common import time_stamp, str_to_list, field_cache, ProxyBuilder
def default_jm_logging(topic: str, msg: str):
from common import format_ts
print(f'{format_ts()}:【{topic}{msg}')
from common import format_ts, current_thread
print('[{}] [{}]:【{}{}'.format(format_ts(), current_thread().name, topic, msg))
# 禁漫常量
@ -24,17 +24,18 @@ class JmMagicConstants:
TIME_MONTH = 'm'
TIME_ALL = 'a'
# 全部, 同人, 单本, 短篇, 其他, 韩漫, 美漫, cosplay, 3D
# category = ["0", "doujin", "single", "short", "another", "hanman", "meiman", "doujin_cosplay", "3D"]
CATEGORY_ALL = '0'
CATEGORY_DOUJIN = 'doujin'
CATEGORY_SINGLE = 'single'
CATEGORY_SHORT = 'short'
CATEGORY_ANOTHER = 'another'
CATEGORY_HANMAN = 'hanman'
CATEGORY_MEIMAN = 'meiman'
CATEGORY_DOUJIN_COSPLAY = 'doujin_cosplay'
CATEGORY_3D = '3D'
# 分类参数API接口的category
CATEGORY_ALL = '0' # 全部
CATEGORY_DOUJIN = 'doujin' # 同人
CATEGORY_SINGLE = 'single' # 单本
CATEGORY_SHORT = 'short' # 短篇
CATEGORY_ANOTHER = 'another' # 其他
CATEGORY_HANMAN = 'hanman' # 韩漫
CATEGORY_MEIMAN = 'meiman' # 美漫
CATEGORY_DOUJIN_COSPLAY = 'doujin_cosplay' # cosplay
CATEGORY_3D = '3D' # 3D
CATEGORY_ENGLISH_SITE = 'english_site' # 英文站
CATEGORY_JM_TEAM = '禁漫漢化組'
# 分页大小
PAGE_SIZE_SEARCH = 80
@ -52,10 +53,10 @@ class JmMagicConstants:
APP_TOKEN_SECRET = '18comicAPP'
APP_TOKEN_SECRET_2 = '18comicAPPContent'
APP_DATA_SECRET = '185Hcomic3PAPP7R'
APP_VERSION = '1.6.6'
APP_VERSION = '1.6.7'
APP_HEADERS_TEMPLATE = {
'Accept-Encoding': 'gzip',
'user-agent': 'Mozilla/5.0 (Linux; Android 9; V1938CT Build/PQ3A.190705.09211555; wv) AppleWebKit/537.36 (KHTML, '
'user-agent': 'Mozilla/5.0 (Linux; Android 9; V1938CT Build/PQ3A.190705.11211812; wv) AppleWebKit/537.36 (KHTML, '
'like Gecko) Version/4.0 Chrome/91.0.4472.114 Safari/537.36',
}

View File

@ -50,8 +50,10 @@ class JmDownloader(DownloadCallback):
def __init__(self, option: JmOption) -> None:
self.option = option
# 收集所有下载的image为plugin提供数据
self.all_downloaded: Dict[JmAlbumDetail, Dict[JmPhotoDetail, List[Tuple[str, JmImageDetail]]]] = {}
# 下载成功的记录dict
self.download_success_dict: Dict[JmAlbumDetail, Dict[JmPhotoDetail, List[Tuple[str, JmImageDetail]]]] = {}
# 下载失败的记录list
self.download_failed_list: List[Tuple[JmImageDetail, BaseException]] = []
def download_album(self, album_id):
client = self.client_for_album(album_id)
@ -101,11 +103,21 @@ class JmDownloader(DownloadCallback):
if use_cache is True and image.is_exists:
return
e = None
try:
client.download_by_image_detail(
image,
img_save_path,
decode_image=decode_image,
)
except BaseException as e:
jm_log('image.failed', f'图片下载失败: [{image.download_url}], 异常: {e}')
# 保存失败记录
self.download_failed_list.append((image, e))
if e is not None:
raise e
self.after_image(image, img_save_path)
# noinspection PyMethodMayBeStatic
@ -164,11 +176,33 @@ class JmDownloader(DownloadCallback):
"""
return self.option.build_jm_client()
@property
def all_success(self) -> bool:
"""
是否成功下载了全部图片
该属性需要等到downloader的全部download_xxx方法完成后才有意义
注意如果使用了filter机制例如通过filter只下载3张图片那么all_success也会为False
"""
if len(self.download_failed_list) != 0:
return False
for album, photo_dict in self.download_success_dict.items():
if len(album) != len(photo_dict):
return False
for photo, image_list in photo_dict.items():
if len(photo) != len(image_list):
return False
return True
# 下面是回调方法
def before_album(self, album: JmAlbumDetail):
super().before_album(album)
self.all_downloaded.setdefault(album, {})
self.download_success_dict.setdefault(album, {})
self.option.call_all_plugin(
'before_album',
album=album,
@ -185,8 +219,8 @@ class JmDownloader(DownloadCallback):
def before_photo(self, photo: JmPhotoDetail):
super().before_photo(photo)
self.all_downloaded.setdefault(photo.from_album, {})
self.all_downloaded[photo.from_album].setdefault(photo, [])
self.download_success_dict.setdefault(photo.from_album, {})
self.download_success_dict[photo.from_album].setdefault(photo, [])
self.option.call_all_plugin(
'before_photo',
photo=photo,
@ -214,7 +248,7 @@ class JmDownloader(DownloadCallback):
photo = image.from_photo
album = photo.from_album
self.all_downloaded.get(album).get(photo).append((img_save_path, image))
self.download_success_dict.get(album).get(photo).append((img_save_path, image))
self.option.call_all_plugin(
'after_image',
image=image,

View File

@ -105,13 +105,11 @@ class DirRule:
解析下载路径dsl得到一个路径规则解析列表
"""
if '_' not in rule_dsl and rule_dsl != 'Bd':
ExceptionTool.raises(f'不支持的dsl: "{rule_dsl}"')
rule_list = rule_dsl.split('_')
rule_list = self.split_rule_dsl(rule_dsl)
solver_ls: List[DirRule.RuleSolver] = []
for rule in rule_list:
rule = rule.strip()
if rule == 'Bd':
solver_ls.append((0, lambda _: base_dir, 'Bd'))
continue
@ -124,6 +122,19 @@ class DirRule:
return solver_ls
# noinspection PyMethodMayBeStatic
def split_rule_dsl(self, rule_dsl: str) -> List[str]:
if rule_dsl == 'Bd':
return [rule_dsl]
if '/' in rule_dsl:
return rule_dsl.split('/')
if '_' in rule_dsl:
return rule_dsl.split('_')
ExceptionTool.raises(f'不支持的rule配置: "{rule_dsl}"')
@classmethod
def get_rule_solver(cls, rule: str) -> Optional[RuleSolver]:
# 查找缓存
@ -181,6 +192,7 @@ class JmOption:
client: Dict,
plugins: Dict,
filepath=None,
call_after_init_plugin=True,
):
# 路径规则配置
self.dir_rule = DirRule(**dir_rule)
@ -196,8 +208,22 @@ class JmOption:
# 需要主线程等待完成的插件
self.need_wait_plugins = []
if call_after_init_plugin:
self.call_all_plugin('after_init', safe=True)
def copy_option(self):
return self.__class__(
dir_rule={
'rule': self.dir_rule.rule_dsl,
'base_dir': self.dir_rule.base_dir,
},
download=self.download.src_dict,
client=self.client.src_dict,
plugins=self.plugins.src_dict,
filepath=self.filepath,
call_after_init_plugin=False
)
"""
下面是decide系列方法为了支持重写和增加程序动态性
"""

View File

@ -298,7 +298,7 @@ class ZipPlugin(JmOptionPlugin):
# 原文件夹 -> zip文件
dir_zip_dict: Dict[str, Optional[str]] = {}
photo_dict = downloader.all_downloaded[album]
photo_dict = downloader.download_success_dict[album]
if level == 'album':
zip_path = self.get_zip_path(album, None, filename_rule, suffix, zip_dir)
@ -383,7 +383,7 @@ class ZipPlugin(JmOptionPlugin):
dirs = sorted(dir_zip_dict.keys(), reverse=True)
image_paths = [
path
for photo_dict in self.downloader.all_downloaded.values()
for photo_dict in self.downloader.download_success_dict.values()
for image_list in photo_dict.values()
for path, image in image_list
]
@ -752,7 +752,7 @@ class ConvertJpgToPdfPlugin(JmOptionPlugin):
paths = [
path
for path, image in downloader.all_downloaded[photo.from_album][photo]
for path, image in downloader.download_success_dict[photo.from_album][photo]
]
paths.append(self.option.decide_image_save_dir(photo, ensure_exists=False))

View File

@ -472,7 +472,7 @@ class JmPageTool:
return JmFavoritePage(content, folder_list, total)
@classmethod
def parse_api_to_search_page(cls, data: DictModel) -> JmSearchPage:
def parse_api_to_search_page(cls, data: AdvancedEasyAccessDict) -> JmSearchPage:
"""
model_data: {
"search_query": "MANA",
@ -501,7 +501,7 @@ class JmPageTool:
return JmSearchPage(content, total)
@classmethod
def parse_api_to_favorite_page(cls, data: DictModel) -> JmFavoritePage:
def parse_api_to_favorite_page(cls, data: AdvancedEasyAccessDict) -> JmFavoritePage:
"""
{
"list": [
@ -546,7 +546,7 @@ class JmPageTool:
@classmethod
def adapt_content(cls, content):
def adapt_item(item: DictModel):
def adapt_item(item: AdvancedEasyAccessDict):
item: dict = item.src_dict
item.setdefault('tags', [])
return item
@ -673,7 +673,7 @@ class JmApiAdaptTool:
series = data['series']
episode_list = []
for chapter in series:
chapter = DictModel(chapter)
chapter = AdvancedEasyAccessDict(chapter)
# photo_id, photo_index, photo_title, photo_pub_date
episode_list.append(
(chapter.id, chapter.sort, chapter.name, None)
@ -688,7 +688,7 @@ class JmApiAdaptTool:
sort = 1
series: list = data['series'] # series中的sort从1开始
for chapter in series:
chapter = DictModel(chapter)
chapter = AdvancedEasyAccessDict(chapter)
if int(chapter.id) == int(data['id']):
sort = chapter.sort
break