diff --git a/.github/workflows/pica_crawler_actions.yml b/.github/workflows/pica_crawler_actions.yml
index 3984c3ab..c4309d7d 100644
--- a/.github/workflows/pica_crawler_actions.yml
+++ b/.github/workflows/pica_crawler_actions.yml
@@ -23,23 +23,12 @@ jobs:
PICA_SECRET_KEY: ${{secrets.PICA_SECRET_KEY}}
PICA_ACCOUNT: ${{secrets.PICA_ACCOUNT}}
PICA_PASSWORD: ${{secrets.PICA_PASSWORD}}
- # 过滤分区 用,分隔
- CATEGORIES: CG雜圖,生肉,耽美花園,偽娘哲學,扶他樂園,性轉換,SAO 刀劍神域,WEBTOON,Cosplay
- # CATEGORIES_RULE 过滤规则 INCLUDE: 包含任意一个分区就下载 EXCLUDE: 包含任意一个分区就不下载
- CATEGORIES_RULE: EXCLUDE
- # 订阅的关键词,会下载x天范围内上传的漫画 为空则关闭关键词订阅 用,分隔
- SUBSCRIBE_KEYWORD: ひぐま屋 (野良ヒグマ),アキレルショウジョ,オクモト悠太,ゐちぼっち,黒本君,もすきーと音
- # 订阅的x天范围 git actions运行时填小一点,免得漫画过多邮箱推送不了,本地运行时随便填
- SUBSCRIBE_DAYS: 7
# 允许在下载完成后发送自定义消息,为空则不发送 例: https://api.day.app/{your_keys}/picacg下载成功
BARK_URL: ${{secrets.BARK_URL}}
- #每下载一本漫画的间隔时间(秒),在下载大量漫画时可以设置的稍微大一些,免得哔咔服务器响应不过来
- INTERVAL_TIME: 5
- #下载阶段是否输出细节信息
- DETAIL: True
run: |
python ./src/main.py
git add ./data/downloaded.db
+ git add ./logs
# comics文件夹下的所有漫画都会被打成一个压缩包,并上传到actions artifact. 如果不配置邮箱推送功能,可以用这个来下载到漫画
- name: upload-artifact
uses: actions/upload-artifact@v4
diff --git a/src/Dockerfile b/Dockerfile
similarity index 55%
rename from src/Dockerfile
rename to Dockerfile
index 38cd1176..f0a52455 100644
--- a/src/Dockerfile
+++ b/Dockerfile
@@ -8,21 +8,14 @@ WORKDIR /app
#RUN apt-get update && apt-get install -y build-essential
# 设置环境变量
-ENV PICA_SECRET_KEY="" \
- REQUEST_PROXY="" \
- PACKAGE_TYPE="False" \
- BARK_URL="" \
- INTERVAL_TIME="5" \
- DETAIL="False" \
- REQUEST_TIME_OUT="10" \
- CHANGE_FAVOURITE="False" \
+ENV PACKAGE_TYPE="False" \
DELETE_COMIC="True"
# 将当前目录内容复制到工作目录中
COPY . /app
# 安装依赖项
-RUN pip install --no-cache-dir requests urllib3
+RUN pip install --progress-bar off requests urllib3
# 指定容器启动时执行的命令
-CMD ["python", "main.py"]
+CMD ["python", "./src/main.py"]
diff --git a/README.md b/README.md
index ab2f31e7..0c4268f6 100644
--- a/README.md
+++ b/README.md
@@ -3,116 +3,96 @@
一个哔咔漫画的下载程序,基于python实现,欢迎各位绅士来捉虫
* 目前已实现按 排行榜/收藏夹/指定关键词 进行下载的功能
* 本项目是基于[AnkiKong大佬开源的项目](https://github.com/AnkiKong/picacomic)编写的,仅供技术研究使用,请勿用于其他用途,有问题可以提issue
-* 可以fork这个项目,根据[api文档](https://www.apifox.cn/apidoc/shared-44da213e-98f7-4587-a75e-db998ed067ad/doc-1034189)自行开发功能
* 麻烦给个star支持一下:heart:
-# 本地运行
+## 下载的范围
-漫画直接下载到本地磁盘,免去了邮箱推送这个步骤
-需要手动运行,不支持定时运行,适合下载大量漫画
-1. clone项目到本地
-2. 把pica_crawler_actions.yml的`env`中所有环境变量配置到本地
-3. 开启科学上网
-4. 参考`./config/config_backup.ini`构建个人配置文件`./config/config.ini`
-5. 运行`main.py`,下载好的漫画在`/comics`这个文件夹内
-6. 下载过的漫画将被维护进`data/downloaded.db`文件中
+### 排行榜
+哔咔24小时排行榜内的所有漫画
+### 收藏夹
+收藏夹内的所有漫画,下载完成是否取消收藏取决于配置文件中的`CHANGE_FAVOURITE`, if ="True":自动取消收藏; elif ="False":不取消收藏
-# docker 运行
+### 关键词订阅
+`./config/config.ini`里的`subscribe_keyword`里配置若干个关键词,下载范围等同于在哔咔app里用关键词搜索到的所有漫画
-新增了环境变量 `PACKAGE_TYPE`, 参数为 True 和 False
-设置为True时, 会根据漫画名称压缩成zip包, 以供 Komga 等漫画库 使用, 也会删除comics文件夹 ( 避免docker容器占用过多硬盘 )
-```python
-# main.py
-if os.environ.get("PACKAGE_TYPE", "False") == "True":
- # 打包成zip文件, 并删除旧数据
- zip_subfolders('./comics', './output')
- shutil.rmtree('./comics')
-```
+### 部分漫画不会被下载的原因
+排行榜/订阅的漫画会受到以下过滤逻辑的影响,**收藏夹则不会**(如果下载到本地后文件丢失了,可以通过放入收藏夹把它全量下载下来)
-新增了环境变量 `REQUEST_PROXY`, 这样下载图片时允许使用代理了
-```python
-# client.py
-proxy = os.environ.get("REQUEST_PROXY")
-if proxy:
- proxies = {'http': proxy, 'https': proxy}
-else:
- proxies = None
-response = self.__s.request(method=method, url=url, verify=False, proxies=proxies, **kwargs)
-return response
-```
+#### 过滤重复下载
-新增了环境变量 `BARK_URL`, bark消息通知
- 允许打包完成 or 下载完成发送自定义消息, 例: `https://api.day.app/{your_keys}/picacg下载成功`
-```python
-# main.py
-if os.environ.get("BARK_URL"):
- # 发送消息通知
- request.get(os.environ.get("BARK_URL"))
-```
+`./data/downloaded.db`文件记录了已扫描过的漫画id, 并记录了成功下载过的漫画章节名和基本信息.
+如果数据库文件中存在该漫画id, **则触发增量下载,跳过已下载的章节**, 否则触发全量下载.
+若采用GitHub Actions方式运行, 会将`./data/downloaded.db`文件提交到代码仓库以保存本次的运行结果. 若`GIT_TOKEN`配置错误则代码提交失败,从而导致漫画被重复下载和推送
-可以挂载这两个目录
-工作目录为 `/app/comics` 存放下载漫画图片的文件夹, `/app/output` 存放输出zip的文件夹
+#### 过滤分区
+`config.ini`文件的``CATEGORIES``配置项可以配置0~n个哔咔的分区, 配置为空则代表不过滤
-1. `docker-compose.yml` 参考 docker-compose.yml 文件
+``CATEGORIES_RULE``可以配置为 INCLUDE: 包含任意一个分区就下载 EXCLUDE: 包含任意一个分区就不下载
-2. `docker cli` 最小运行
+> 部分漫画只打上了'短篇'/'长篇'这样单个分区,在配置为``INCLUDE``时,建议把比较常见的分区给填上,不然容易匹配不到漫画
-PICA_SECRET_KEY可以不用更改, 如果需要更改时, 注意是单引号内容
-新增了环境变量 `REQUEST_TIME_OUT`, 自定义限制每次请求最大时间;新增了环境变量 `DETAIL`, 下载阶段是否输出细节信息;新增了 `CHANGE_FAVOURITE`, 是否自动删除收藏夹;新增环境变量 `DELETE_COMIC`, 是否打包后删除漫画;
+#### 订阅的时间范围
+对于订阅的漫画,如果 当天 - 订阅漫画的上传日 > `subscribe_days`,这本漫画将不再被下载
-docker部署建议将PACKAGE_TYPE打开, 同时挂载/app/output目录
-```docker
-docker run --name picacg-download-container -d \
- -e PICA_ACCOUNT="账户名称" \
- -e PICA_PASSWORD="账户密码" \
- -e REQUEST_PROXY="http代理(可选)" \
- -e BARK_URL="bark消息通知(可选)" \
- -e PACKAGE_TYPE="True" \
- -e REQUEST_TIME_OUT="限制时间s: int(可选)"
- -e DETAIL="False" \
- -e CHANGE_FAVOURITE="True" \
- -e DELETE_COMIC="False" \
- -e PICA_SECRET_KEY='~d}$Q7$eIni=V)9\RK/P.RM4;9[7|@/CA}b~OW!3?EV`:<>M7pddUBL5n|0/*Cn' \
- -v ./comics:/app/comics \
- -v ./output:/app/output \
- yuanzhangzcc/picacg-download:latest
+
+#### 检查日志文件
+在文件夹`logs`下存储有运行日志文件,所有日志按天自动划分,自动删除超过`backup_count`天的日志。错误信息单独保存到`ERROR_*.log`中。
+
+# 运行方式
+
+## 本地运行
+1. 参考`./config/config_backup.ini`构建个人配置文件`./config/config.ini`,缺少该文件项目将无法运行
+2. 开启科学上网
+3. 在项目根目录下运行`python ./src/main.py`. 漫画将被下载到`./comics`文件夹下, 同时将已下载的漫画维护进`./data/downloaded.db`文件中
+
+## Docker 运行
+> 因为`./config/config.ini`要配置的东西有点多, 推荐把项目clone下来, 改完配置后自行构建镜像
+
+1. 参考`./config/config_backup.ini`构建个人配置文件`./config/config.ini`,缺少该文件项目将无法运行
+2. 修改`Dockerfile`文件中ENV环境变量的配置
+`PACKAGE_TYPE`: 参数为 True / False. 设置为True时, 会根据漫画名称压缩成zip包, 以供 Komga 等漫画库使用
+`DELETE_COMIC`: 参数为 True / False. 设置为True时, 会在压缩包生成后删除文件夹下的漫画( 避免docker容器占用过多硬盘 )
+3. 在项目根目录下运行`docker build -t picacg-download:latest .`
+4. 启动容器的脚本
+```shell
+docker run --name picacg-download-container -d
+ -v ./comics:/app/comics #挂载存放下载漫画图片的文件夹
+ -v ./output:/app/output #挂载存放压缩包的文件夹(如果配置了需要打包)
+ #-e 添加环境变量可以覆盖config.ini中的配置, 免去重新build的操作
+ picacg-download:latest
```
-# git actions运行
+## GitHub Actions运行
-漫画将会以压缩包附件的形式推送到邮箱上,受限于邮件的附件大小,漫画会被打包为若干个压缩包,一次性可能会收到若干个邮件
+漫画将会以压缩包附件的形式推送到邮箱上,受限于邮件的附件大小限制,漫画会被打包为若干个压缩包,一次性可能会收到若干个邮件
不同邮箱支持的最大邮件内容不同,qq/新浪是50mb,outlook是20mb,建议用大一点的,避免拆分的压缩包过多下载起来麻烦
-支持自动定时运行,适合每天推送少量的漫画
-* fork本仓库
-* 新增Actions secrets
+支持自动定时运行,无需搭建个人服务器,适合每天推送少量的漫画
+1. fork本仓库
+2. 新增Actions secrets
-| secret | 说明 |
+| Actions secrets | 说明 |
| --------------- | ------------------------------------------------------------ |
| PICA_SECRET_KEY | [AnkiKong提供的secret_key](https://zhuanlan.zhihu.com/p/547321040) |
| PICA_ACCOUNT | 哔咔登录的账号 |
| PICA_PASSWORD | 哔咔登录的密码 |
| EMAIL_ACCOUNT | 接收漫画的邮箱 |
-| BARK_URL | 允许打包完成 or 下载完成发送自定义消息 例: `https://api.day.app/{your_keys}/picacg下载成功` |
| EMAIL_AUTH_CODE | 邮箱的授权码,[参考qq邮箱的这篇文档](https://service.mail.qq.com/cgi-bin/help?subtype=1&&id=28&&no=1001256) |
| GIT_TOKEN | [参考这篇文章](http://t.zoukankan.com/joe235-p-15152380.html),只勾选repo的权限,Expiration设置为No Expiration |
+3. 参考`./config/config_backup.ini`调整个人配置文件`./config/config.ini`并上传至GitHub代码仓库(Actions secrets中已配置的内容空着不填即可)
-
-* 打开fork项目的workFlow开关
+4. 打开fork项目的workFlow开关
-* 点击pica_crawler_actions.yml,编辑git actions. 写了注释的配置项,都可以根据需求改动
-
-
+5. 修改`./.github/workflows/pica_crawler_actions.yml`配置文件. 如果需要邮箱推送,请修改`EMAIL_SERVER_HOST`等邮箱相关的配置
-
-* 手动触发一次,测试下能不能跑通
+6. 手动触发一次,测试下能不能跑通
@@ -120,60 +100,18 @@ docker run --name picacg-download-container -d \
**成功运行的截图:**
-* 成功运行后,可以在这里下载到漫画的压缩包. 如果配置了邮箱推送功能,还可以查收邮件里的附件
+7. 成功运行后,可以在这里下载到漫画的压缩包. 如果配置了邮箱推送功能,还可以查收邮件里的附件
* [我自己也fork了一份](https://github.com/PhantomStrikers/pica_crawler.git),每天都在自动运行的,可以通过这个项目的actions运行记录判断这个项目是否还能work
-
-# 解压注意事项
+### 邮件压缩包的解压注意事项
1. 将所有邮件的压缩包下载至统一目录. (存在单个压缩包里可能只有半本漫画的情况),然后**全选**压缩包,右键**解压到当前文件夹**.
-2. 如果你在上个步骤选择右键解压文件,默认是以压缩包名创建一个新的文件夹,会出现漫画拆散在不同文件夹的情况
+2. 如果你在上个步骤选择右键**解压文件**,默认是以压缩包名创建一个新的文件夹,会出现漫画被拆散在不同文件夹的情况
3. 压缩包默认zip格式,无解压密码.遇到解不开的情况可能是下载时压缩包损坏了,尝试下重新下载
-# 下载的范围
-
-## 排行榜
-哔咔24小时排行榜内的所有漫画
-
-## 收藏夹
-收藏夹内的所有漫画,下载完成是否取消收藏取决于环境变量`CHANGE_FAVOURITE`, if ="True":自动取消收藏; elif ="False":不取消收藏
-
-## 关键词订阅
-`./config/config.ini`里的`subscribe_keyword`里配置若干个关键词,下载范围等同于在哔咔app里用关键词搜索到的所有漫画
-这个功能可能会下载过量的漫画,导致邮箱无法推送,可以调整`subscribe_days`缩小下载范围,或者是本地运行`main.py`
-
-# 部分漫画不会被下载的原因
-排行榜/订阅的漫画会受到以下过滤逻辑的影响,**收藏夹则不会**(如果下载到本地后文件丢失了,可以通过放入收藏夹把它全量下载下来)
-
-
-### 过滤重复下载
-
-`./data/downloaded.db`文件记录了已扫描过的漫画id, 并记录了成功下载过的漫画章节名和基本信息.
-**排行榜上已下载过的漫画会触发增量下载,跳过曾下载过的章节**,其余所有情况都是全量下载所有章节.
-每次运行代码后,都会通过git actions的指令提交代码,保存本次的运行结果.`GIT_TOKEN`配置错误将导致提交代码失败,这会导致漫画被重复下载和推送
-
-
-### 过滤分区
-
-支持通过分区自定义下载逻辑.
-
-git actions配置文件的``CATEGORIES``配置项可以配置0~n个哔咔的分区, 配置为空则代表不过滤
-
-``CATEGORIES_RULE``可以配置为 INCLUDE: 包含任意一个分区就下载 EXCLUDE: 包含任意一个分区就不下载
-
-> 部分漫画只打上了'短篇'/'长篇'这样单个分区,在配置为``INCLUDE``时,建议把比较常见的分区给填上,不然容易匹配不到漫画
-
-
-### 订阅的时间范围
-对于订阅的漫画,如果 当天 - 订阅漫画的上传日 > `subscribe_days`,这本漫画将不再被下载
-
-
-### 日志文件
-在文件夹`logs`下存储有运行日志文件,所有日志按天自动划分,自动删除超过`backup_count`天的日志。错误信息单独保存到`ERROR_*.log`中。
-
# 结尾
[清心寡欲在平时,坚守临期凛四知,鸩酒岂堪求止渴,光明正大好男儿:thumbsup:](https://tieba.baidu.com/f?kw=%E6%88%92%E8%89%B2&ie=utf-8)
@@ -190,9 +128,9 @@ git actions配置文件的``CATEGORIES``配置项可以配置0~n个哔咔的分
| 2023/10/02 | 1.补充了上次更新没写进去的运行时间的保存逻辑:laughing: 2.改用total_seconds()判断时间差,seconds算出来的结果有误 3.修复了分卷压缩函数KeyError的问题--创建的压缩包个数 = 文件总大小/压缩包的最大大小,但分卷压缩时每个包都不会被填满的,导致实际需要更多的包 |
| 2023/09/12 | 修复了漫画曾被下载一次后, 新增章节无法被下载的bug. 现在记录了上次运行的时间, 与漫画章节的上传时间进行比对, 有新章节时则触发增量下载 |
| 2023/03/28 | 修复了调用分页获取章节接口时,只获取了第一页的bug,这会导致总章节数>40的漫画下载不全 |
-| 2023/02/03 | 参考了[jiayaoO3O/18-comic-finder](https://github.com/jiayaoO3O/18-comic-finder),现在可以在git actions上下载到完整的压缩包 |
-| 2023/02/01 | 1.区分了git actions和本地运行两种运行方式 2.新增按关键词订阅功能 3.调整了邮箱的配置项,支持指定加密方式和端口 4.引入`狗屁不通文章`生成邮件正文的随机字符串 |
-| 2023/01/06 | 基于git actions重构了代码,并采用了邮件的形式推送漫画 |
+| 2023/02/03 | 参考了[jiayaoO3O/18-comic-finder](https://github.com/jiayaoO3O/18-comic-finder),现在可以在GitHub Actions上下载到完整的压缩包 |
+| 2023/02/01 | 1.区分了GitHub Actions和本地运行两种运行方式 2.新增按关键词订阅功能 3.调整了邮箱的配置项,支持指定加密方式和端口 4.引入`狗屁不通文章`生成邮件正文的随机字符串 |
+| 2023/01/06 | 基于GitHub Actions重构了代码,并采用了邮件的形式推送漫画 |
| 2022/12/08 | 启动项目时自动打卡 |
| 2022/11/27 | 实现按排行榜以及收藏夹进行下载的功能 |
diff --git a/config/config.ini b/config/config.ini
new file mode 100644
index 00000000..38d0cc6f
--- /dev/null
+++ b/config/config.ini
@@ -0,0 +1,56 @@
+#按实际需求改param,filter这两节下的配置即可,其余配置不改不影响运行
+#请不要将哔咔账密/哔咔密钥等敏感信息上传至GitHub 请不要将哔咔账密/哔咔密钥等敏感信息上传至GitHub 请不要将哔咔账密/哔咔密钥等敏感信息上传至GitHub
+
+[param]
+#哔咔账户名称
+pica_account:
+#哔咔账户密码
+pica_password:
+#哔咔请求时的密钥 参考AnkiKong提供的secret_key https://zhuanlan.zhihu.com/p/547321040
+pica_secret_key:
+#在下载完成后,是否自动取消收藏(推荐为True,避免收藏夹下的漫画被重复下载)
+change_favourite: True
+#运行结束后发送自定义消息进行通知(可为空) 例: `https://api.day.app/{your_keys}/picacg下载成功`
+bark_url:
+#使用代理(为空则不使用代理)
+request_proxy:
+#将同一本漫画的不同章节进行合并
+merge_episodes: False
+
+#自定义过滤规则
+[filter]
+#过滤分区 具体的分区列表可以从client.py的categories方法中获取 为空则不过滤 用,分隔
+categories: CG雜圖,生肉,耽美花園,偽娘哲學,扶他樂園,性轉換,SAO 刀劍神域,WEBTOON,Cosplay
+#过滤规则 INCLUDE: 包含任意一个分区就下载 EXCLUDE: 包含任意一个分区就不下载
+categories_rule: EXCLUDE
+# 订阅的关键词,会下载x天范围内上传的漫画 为空则不订阅 用,分隔
+subscribe_keyword:
+# 订阅的x天范围 GitHub Actions运行时填小一点,免得漫画过多邮箱推送不了,本地运行时随便填
+subscribe_days: 60
+
+#下载相关的配置
+[crawl]
+#下载同一本漫画中若干图片的线程并发数.下载是IO密集型操作,可以考虑设置为比cpu核心数稍大一些的值
+concurrency: 5
+#每下载一本漫画的间隔时间(秒),在下载大量漫画时可以设置的稍微大一些,免得哔咔服务器响应不过来
+interval_time: 5
+#下载阶段是否输出细节信息
+detail: True
+#限制每次请求的最大时间(秒)
+request_time_out: 10
+# 保留最近?天的日志文件
+backup_count: 30
+
+#访问哔咔服务器的固定请求头
+[header]
+api-key: C69BAF41DA5ABD1FFEDC6D2FEA56B
+accept: application/vnd.picacomic.com.v1+json
+app-channel: 2
+nonce: b1ab87b4800d4d4590a11701b8551afa
+app-version: 2.2.1.2.3.3
+app-uuid: defaultUuid
+app-platform: android
+app-build-version: 45
+Content-Type: application/json; charset=UTF-8
+User-Agent: okhttp/3.8.1
+image-quality: original
diff --git a/config/config_backup.ini b/config/config_backup.ini
index 481ee0ca..eebe5cf5 100644
--- a/config/config_backup.ini
+++ b/config/config_backup.ini
@@ -1,8 +1,48 @@
+#请根据这个文件配置`./config/config.ini`
+#按实际需求改param,filter这两节下的配置即可,其余配置不改不影响运行
+#请不要将哔咔账密/哔咔密钥等敏感信息上传至GitHub 请不要将哔咔账密/哔咔密钥等敏感信息上传至GitHub 请不要将哔咔账密/哔咔密钥等敏感信息上传至GitHub
+
+[param]
+#哔咔账户名称
+pica_account:
+#哔咔账户密码
+pica_password:
+#哔咔请求时的密钥 参考AnkiKong提供的secret_key https://zhuanlan.zhihu.com/p/547321040
+pica_secret_key:
+#在下载完成后,是否自动取消收藏(推荐为True,避免收藏夹下的漫画被重复下载)
+change_favourite: True
+#运行结束后发送自定义消息进行通知,为空则不通知 例: `https://api.day.app/{your_keys}/picacg下载成功`
+bark_url:
+#使用代理(为空则不使用代理)
+request_proxy:
+#将同一本漫画的不同章节进行合并
+merge_episodes: False
+
+#自定义过滤规则
+[filter]
+# 过滤分区,示例中有目前所有的分区,最新的分区列表可以从client.py的categories方法中获取 为空则不过滤 用,分隔
+categories: 全彩,長篇,同人,短篇,圓神領域,碧藍幻想,CG雜圖,英語 ENG,生肉,純愛,百合花園,耽美花園,偽娘哲學,後宮閃光,扶他樂園,單行本,姐姐系,妹妹系,SM,性轉換,足の恋,人妻,NTR,強暴,非人類,艦隊收藏,Love Live,SAO 刀劍神域,Fate,東方,WEBTOON,禁書目錄,歐美,Cosplay,重口地帶
+#过滤规则 填了categories参数的话这个必填 INCLUDE: 包含任意一个分区就下载 EXCLUDE: 包含任意一个分区就不下载
+categories_rule: EXCLUDE
+# 订阅的关键词,会下载x天范围内上传的漫画 为空则不订阅 用,分隔
+subscribe_keyword:
+# 订阅的x天范围 GitHub Actions运行时填小一点,免得漫画过多邮箱推送不了,本地运行时随便填
+subscribe_days: 60
+
+#下载相关的配置
[crawl]
-#下载同一本的不同分页的图片的并发数,并不是多本同时并发下载 具体多少并发数自己定义
-concurrency = 5
+#下载同一本漫画中若干图片的线程并发数.下载是IO密集型操作,可以考虑设置为比cpu核心数稍大一些的值
+concurrency: 5
+#每下载一本漫画的间隔时间(秒),在下载大量漫画时可以设置的稍微大一些,免得哔咔服务器响应不过来
+interval_time: 5
+#下载阶段是否输出细节信息
+detail: True
+#限制每次请求的最大时间(秒)
+request_time_out: 10
+# 保留最近?天的日志文件
+backup_count: 30
-#固定的请求头
+#访问哔咔服务器的固定请求头
[header]
api-key: C69BAF41DA5ABD1FFEDC6D2FEA56B
accept: application/vnd.picacomic.com.v1+json
@@ -16,18 +56,3 @@ Content-Type: application/json; charset=UTF-8
User-Agent: okhttp/3.8.1
image-quality: original
-[param]
-#哔咔账户名称
-pica_account:
-#哔咔账户密码
-pica_password:
-#过滤分区 用,分隔
-categories: A,B
-#过滤规则 INCLUDE: 包含任意一个分区就下载 EXCLUDE: 包含任意一个分区就不下载
-categories_rule: EXCLUDE
-# 订阅的关键词,会下载x天范围内上传的漫画 为空则关闭关键词订阅 用,分隔
-subscribe_keyword: A,B
-# 订阅的x天范围 git actions运行时填小一点,免得漫画过多邮箱推送不了,本地运行时随便填
-subscribe_days: 60
-# 保留最近?天的日志文件
-backup_count: 30
diff --git a/docker-compose.yml b/docker-compose.yml
index fa902948..e6e69a4e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -4,19 +4,16 @@ services:
image: yuanzhangzcc/picacg-download:latest
container_name: picacg-download-container
volumes:
- - ./comics/bika:/app/comics
+ - ./comics:/app/comics
- ./output:/app/output
- ./logs:/app/logs
- ./data:/app/data
- ./config:/app/config
environment:
- - PICA_SECRET_KEY=~d}$$Q7$$eIni=V)9\RK/P.RM4;9[7|@/CA}b~OW!3?EV`:<>M7pddUBL5n|0/*Cn #$字符存在转义问题,将密钥里的$替换为$$
+ - PICA_SECRET_KEY= #Dockerfile设置环境变量时,$字符好像存在转义问题,需要将密钥里的$替换为$$
- REQUEST_PROXY= #下载图片代理
- PACKAGE_TYPE=True #是否打包为zip, 推荐True
- - INTERVAL_TIME=5 #每下载一本漫画的间隔时间(秒)
- - REQUEST_TIME_OUT=10 #URL请求时间限制
- - DETAIL=False #是否打印详细信息
- - CHANGE_FAVOURITE=False #是否删除收藏夹内容
- DELETE_COMIC=False #是否打包后删除漫画
+ - CHANGE_FAVOURITE=False #是否删除收藏夹内容
- BARK_URL= #下载完成消息通知
restart: unless-stopped
diff --git a/src/client.py b/src/client.py
index a088a2ff..9da362e3 100644
--- a/src/client.py
+++ b/src/client.py
@@ -30,19 +30,19 @@ def __init__(self) -> None:
parser = ConfigParser()
parser.read('./config/config.ini', encoding='utf-8')
self.headers = dict(parser.items('header'))
- self.timeout = int(os.environ.get("REQUEST_TIME_OUT", 10))
+ self.timeout = int(get_cfg("crawl", "request_time_out", 10))
def http_do(self, method, url, **kwargs):
kwargs.setdefault("allow_redirects", True)
header = self.headers.copy()
ts = str(int(time()))
raw = url.replace(base, "") + str(ts) + header["nonce"] + method + header["api-key"]
- hc = hmac.new(os.environ["PICA_SECRET_KEY"].encode(), digestmod=hashlib.sha256)
+ hc = hmac.new(get_cfg("param", "pica_secret_key").encode(), digestmod=hashlib.sha256)
hc.update(raw.lower().encode())
header["signature"] = hc.hexdigest()
header["time"] = ts
kwargs.setdefault("headers", header)
- proxy = os.environ.get("REQUEST_PROXY")
+ proxy = get_cfg("param", "request_proxy")
if proxy:
proxies = {'http': proxy, 'https': proxy}
else:
@@ -153,7 +153,7 @@ def search_all(self, keyword):
datetime.now() -
datetime.strptime(comic["updated_at"], "%Y-%m-%dT%H:%M:%S.%fZ")
).days
- ) <= int(get_cfg('param', 'subscribe_days'))]
+ ) <= int(get_cfg('filter', 'subscribe_days'))]
subscribed_comics += recent_comics
# Check if any comics in the current page exceed the subscribe time limit.
diff --git a/src/data_migration.py b/src/data_migration.py
index ce39ea4b..85e00913 100644
--- a/src/data_migration.py
+++ b/src/data_migration.py
@@ -35,9 +35,9 @@
author = comic["author"]
episodes = pica_server.episodes_all(cid, title)
if episodes:
+ mark_comic_as_downloaded(cid, title)
for episode in episodes:
- mark_comic_as_downloaded(cid, title)
update_downloaded_episodes(cid, episode["title"], db_path)
- update_comic_data(comic, db_path)
+ update_comic_data(comic, db_path)
else:
print(f'该漫画可能已被删除,{cid}')
diff --git a/src/main.py b/src/main.py
index 6ae15bf2..a0e679f6 100644
--- a/src/main.py
+++ b/src/main.py
@@ -1,20 +1,18 @@
# encoding: utf-8
import io
-import json
import sys
-import threading
import time
-import traceback
-import shutil
-import requests
from concurrent.futures import ThreadPoolExecutor, as_completed
-import logging
from logging.handlers import TimedRotatingFileHandler
from pathlib import Path
from client import Pica
from util import *
+#校验config.ini文件是否存在
+config_dir = './config/config.ini'
+if not os.path.isfile('./config/config.ini'):
+ raise Exception(f"配置文件`{config_dir}`不存在,请参考`./config/config_backup.ini`进行配置")
# 配置日志
log_folder = './logs'
@@ -30,7 +28,7 @@ def get_log_filename(name):
get_log_filename('runing'),
when='midnight', # 每天轮转一次
interval=1, # 轮转周期为1天
- backupCount=int(get_cfg('param', 'backup_count')) # 保留最近?天的日志文件
+ backupCount=int(get_cfg('crawl', 'backup_count', 30)) # 保留最近?天的日志文件
)
log_handler.setLevel(logging.INFO)
log_handler.setFormatter(log_formatter)
@@ -41,7 +39,7 @@ def get_log_filename(name):
get_log_filename('ERROR'),
when='midnight', # 每天轮转一次
interval=1, # 轮转周期为1天
- backupCount=int(get_cfg('param', 'backup_count')) # 保留最近?天的日志文件
+ backupCount=int(get_cfg('crawl', 'backup_count', 30)) # 保留最近?天的日志文件
)
error_log_handler.setLevel(logging.ERROR) # 只记录 ERROR 级别及以上的日志
error_log_handler.setFormatter(log_formatter)
@@ -56,13 +54,13 @@ def get_log_filename(name):
# only_latest: true增量下载 false全量下载
-def download_comic(comic, db_path, only_latest):
+def download_comic(comic, db_path, only_latest, executor: ThreadPoolExecutor):
cid = comic["_id"]
title = comic["title"]
author = comic["author"]
categories = comic["categories"]
episodes = pica_server.episodes_all(cid, title)
- is_detail = os.environ.get("DETAIL", "False") == "True"
+ is_detail = get_cfg("crawl", "detail", "True") == "True"
num_pages = comic["pagesCount"] if "pagesCount" in comic else -1
# 增量更新
if only_latest:
@@ -81,17 +79,15 @@ def download_comic(comic, db_path, only_latest):
comic_path = os.path.join(".",
"comics",
f"{convert_file_name(author)}",
- (f"[{convert_file_name(title)}]"
- f"[{convert_file_name(author)}]"
- f"[{convert_file_name(categories)}]")
+ f"{convert_file_name(title)}"
)
comic_path = ensure_valid_path(comic_path)
+
for episode in episodes:
chapter_title = convert_file_name(episode["title"])
chapter_path = os.path.join(comic_path, chapter_title)
chapter_path = Path(chapter_path)
chapter_path.mkdir(parents=True, exist_ok=True)
-
image_urls = []
current_page = 1
while True:
@@ -101,7 +97,7 @@ def download_comic(comic, db_path, only_latest):
current_page += 1
if page_data:
image_urls.extend(list(map(
- lambda i: i['media']['fileServer'] + '/static/' + i['media']['path'],
+ lambda i: i['media']['fileServer'] + '/static/' + i['media']['path'],
page_data
)))
else:
@@ -110,40 +106,36 @@ def download_comic(comic, db_path, only_latest):
logging.error(f"No images found of chapter:{chapter_title} in comic:{title}")
continue
- concurrency = int(get_cfg('crawl', 'concurrency'))
- image_urls_parts = list_partition(image_urls, concurrency)
- downloaded_count = 0.
- for image_urls_part in image_urls_parts:
- with ThreadPoolExecutor(max_workers=concurrency) as executor:
- futures = {
- executor.submit(download,
- pica_server, chapter_path,
- image_urls.index(image_url), image_url
- ): image_url
- for image_url in image_urls_part
- }
- for future in as_completed(futures):
- image_url = futures[future]
- try:
- future.result()
- downloaded_count += 1
- except Exception as e:
- current_image = image_urls.index(image_url) + 1
- episode_title = episode["title"]
- logging.error(f"Error downloading the {current_image}-th image"
- f"in episode:{episode_title}"
- f"in comic:{title}"
- f"Exception:{e}")
- continue
- if is_detail:
+ downloaded_count = 0
+ futures = {
+ executor.submit(download,
+ pica_server, chapter_path,
+ image_urls.index(image_url), image_url
+ ): image_url
+ for image_url in image_urls
+ }
+ for future in as_completed(futures):
+ image_url = futures[future]
+ try:
+ future.result()
+ downloaded_count += 1
+ except Exception as e:
+ current_image = image_urls.index(image_url) + 1
episode_title = episode["title"]
- print(
- f"[episode:{episode_title:<10}] "
- f"downloaded:{downloaded_count:>6}, "
- f"total:{len(image_urls):>4}, "
- f"progress:{int(downloaded_count / len(image_urls) * 100):>3}%",
- flush=True
- )
+ logging.error(f"Error downloading the {current_image}-th image"
+ f"in episode:{episode_title}"
+ f"in comic:{title}"
+ f"Exception:{e}")
+ continue
+ if is_detail:
+ episode_title = episode["title"]
+ print(
+ f"[episode:{episode_title:<10}] "
+ f"downloaded:{downloaded_count:>6}, "
+ f"total:{len(image_urls):>4}, "
+ f"progress:{int(downloaded_count / len(image_urls) * 100):>3}%",
+ flush=True
+ )
if downloaded_count == len(image_urls):
update_downloaded_episodes(cid, episode["title"], db_path)
else:
@@ -154,10 +146,11 @@ def download_comic(comic, db_path, only_latest):
f"Currently, {downloaded_count} images(total_images:{len(image_urls)}) "
"from this episode have been downloaded"
)
-
+ # 下载完成后,根据配置对漫画章节进行合并
+ if get_cfg("param", "merge_episodes", "False") == "True":
+ merge_episodes(comic_path)
# 下载每本漫画的间隔时间
- if os.environ.get("INTERVAL_TIME"):
- time.sleep(int(os.environ.get("INTERVAL_TIME")))
+ time.sleep(int(get_cfg('crawl', "interval_time", 1)))
# 登录并打卡
@@ -174,7 +167,7 @@ def download_comic(comic, db_path, only_latest):
# 关键词订阅的漫画
searched_comics = []
-keywords = get_cfg('param', 'subscribe_keyword').split(',')
+keywords = get_cfg('filter', 'subscribe_keyword').split(',')
for keyword in keywords:
searched_comics_ = pica_server.search_all(keyword)
print('关键词%s: 检索到%d本漫画' % (keyword, len(searched_comics_)), flush=True)
@@ -184,28 +177,31 @@ def download_comic(comic, db_path, only_latest):
favourited_comics = pica_server.my_favourite_all()
print('已下载共计%d本漫画' % get_downloaded_comic_count(db_path), flush=True)
print('收藏夹共计%d本漫画' % (len(favourited_comics)), flush=True)
-isChangeFavo = os.environ.get("CHANGE_FAVOURITE", False) == "True"
-
-for comic in (ranked_comics + favourited_comics + searched_comics):
- try:
- # 收藏夹:全量下载 其余:增量下载
- download_comic(comic, db_path, comic not in favourited_comics)
- info = pica_server.comic_info(comic['_id'])
- # 收藏夹中的漫画被下载后,自动取消收藏,避免下次运行时重复下载
- if info["data"]['comic']['isFavourite'] and isChangeFavo:
- pica_server.favourite(comic["_id"])
- update_comic_data(comic, db_path)
- except Exception as e:
- logging.error(
- 'Download failed for {}, with Exception:{}'.format(comic["title"], e)
- )
- continue
+isChangeFavo = get_cfg("param", "change_favourite", "True") == "True"
+
+concurrency = int(get_cfg('crawl', 'concurrency', 5))
+# 创建线程池的代码不要放for循环里面
+with ThreadPoolExecutor(max_workers=concurrency) as executor:
+ for comic in (ranked_comics + favourited_comics + searched_comics):
+ try:
+ # 收藏夹:全量下载 其余:增量下载
+ download_comic(comic, db_path, comic not in favourited_comics, executor)
+ info = pica_server.comic_info(comic['_id'])
+ # 在收藏夹的漫画下载完成后,根据配置决定是否需要自动取消收藏,避免下次运行时重复下载
+ if info["data"]['comic']['isFavourite'] and isChangeFavo:
+ pica_server.favourite(comic["_id"])
+ update_comic_data(comic, db_path)
+ except Exception as e:
+ logging.error(
+ 'Download failed for {}, with Exception:{}'.format(comic["title"], e)
+ )
+ continue
# 打包成zip文件, 并删除旧数据 , 删除comics文件夹会导致docker挂载报错
if os.environ.get("PACKAGE_TYPE", "False") == "True":
- print("The comic is being packaged")
for folderName in os.listdir('./comics'):
+ print(f"The comic [{folderName}] is being packaged")
folder_path = os.path.join('./comics', folderName)
if os.path.isdir(folder_path):
for chapter_folder in os.listdir(folder_path):
@@ -217,8 +213,8 @@ def download_comic(comic, db_path, only_latest):
# delete folders in comics
if os.environ.get("DELETE_COMIC", "True") == "True":
- print("The comic is being deleted")
for fileName in os.listdir('./comics'):
+ print(f"The comic [{fileName}] is being deleted")
file_path = os.path.join('./comics', fileName)
if os.path.isfile(file_path) or os.path.islink(file_path):
os.unlink(file_path)
@@ -227,9 +223,10 @@ def download_comic(comic, db_path, only_latest):
# 发送消息通知
-if os.environ.get("BARK_URL"):
+bark_url = get_cfg("param", "bark_url")
+if bark_url:
requests.get(
- os.environ.get("BARK_URL") + " " +
+ bark_url + " " +
f"排行榜漫画共计{len(ranked_comics)}" +
f"关键词漫画共计{len(searched_comics)}" +
f"收藏夹漫画共计{len(favourited_comics)}"
diff --git a/src/mergeComic.py b/src/mergeComic.py
deleted file mode 100644
index 68e9238e..00000000
--- a/src/mergeComic.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# 合并多个本子,将同一本放入相同文件夹,按章节顺序升序命名文件夹
-
-import os
-import shutil
-
-from pip._vendor.distlib.compat import raw_input
-
-path = './zips/'
-if not os.path.exists(path):
- os.makedirs(path)
-
-target = raw_input("目标目录:")
-if not os.path.exists(path + target):
- os.makedirs(path + target)
-
-target_files = os.listdir(path + target)
-target_files.sort(key=lambda x: str(x.split('.')[0]))
-index = 1 if not target_files else int(target_files[-1].split('.')[0]) + 1
-
-dirs = os.listdir(path)
-dirs.remove(target)
-for i in range(len(dirs)):
- d = dirs[i]
- pics = os.listdir(path + d)
- pics.sort(key=lambda x: str(x.split('.')[0]))
-
- source = path + d + '/'
- for p in pics:
- os.rename(source + p, path + target + '/' + str(index).zfill(4) + '.jpg')
- index += 1
- shutil.rmtree(source)
- print('merge finished,' + d + ' removed------------------------------------', flush=True)
diff --git a/src/util.py b/src/util.py
index 910193f8..271bf9f1 100644
--- a/src/util.py
+++ b/src/util.py
@@ -1,6 +1,7 @@
import operator
import os
import random
+import shutil
import zipfile
from configparser import ConfigParser
from datetime import datetime
@@ -13,22 +14,33 @@
def convert_file_name(name: str) -> str:
if isinstance(name, list):
name = "&".join(map(str, name))
- # windows的文件夹不能带特殊字符,需要处理下文件夹名
+ # 处理文件夹名中的特殊字符
for i, j in ("//", "\\\", "??", "|︱", "\""", "**", "<<", ">>", ":-"):
name = name.replace(i, j)
name = name.replace(" ", "")
+ # 操作系统对文件夹名最大长度有限制,这里对超长部分进行截断,避免file name too long报错
+ # linux是255字节,windows更大一些
+ name = truncate_string_by_bytes(name, 255)
return name
+# 该方法的配置读取优先级为 环境变量 > config.ini > default_value默认值
+# docker方式部署时, 只需配置环境变量, 无需重新构建镜像
+# GitHub Actions方式部署时, 部分敏感信息不适合填进config.ini文件并上传至代码仓库, 也请配置进环境变量中
+def get_cfg(section: str, key: str, default_value = ''):
+ # 项目中用到的环境变量名统一是大写的, 这里对入参key做了大写转换
+ config_value = os.environ.get(key.upper())
+ if config_value:
+ return config_value
-def get_cfg(section: str, key: str):
+ # 因为ConfigParser限制变量名是小写的, 在读取config.ini的配置,对入参key做了小写转换
parser = ConfigParser()
parser.read('./config/config.ini', encoding='utf-8')
- config_value = dict(parser.items(section))[key]
+ config_value = dict(parser.items(section))[key.lower()]
if config_value:
return config_value
- #如果是用git actions方式部署,部分敏感信息不适合填进config.ini文件并上传至代码仓库,此时可以从从环境变量取值作为兜底
- #ConfigParser读写配置项是按小写来的,但linux环境变量又是大小写敏感的.这里把入参key做了大写转换
- return os.environ[key.upper()]
+
+ # 最后取默认值作为兜底
+ return default_value
def get_latest_run_time():
@@ -44,11 +56,11 @@ def get_latest_run_time():
def filter_comics(comic, episodes, db_path) -> list:
# 已下载过的漫画,执行增量更新
if is_comic_downloaded(comic["_id"], db_path):
- episodes = [episode for episode in episodes
+ episodes = [episode for episode in episodes
if not is_episode_downloaded(comic["_id"], episode["title"], db_path)]
# 过滤掉指定分区的本子
- categories_rule = get_cfg('param', 'categories_rule')
- categories = get_cfg('param', 'categories').split(',')
+ categories_rule = get_cfg('filter', 'categories_rule')
+ categories = get_cfg('filter', 'categories').split(',')
# 漫画的分区和用户自定义分区的交集
intersection = set(comic['categories']).intersection(set(categories))
if categories:
@@ -345,3 +357,87 @@ def ensure_valid_path(path):
print(f"Path too long, truncating: {path}")
path = path[:(max_path_length)] # 截断路径
return path
+
+def truncate_string_by_bytes(s, max_bytes):
+ """
+ 截断字符串,使其字节长度不超过max_bytes。
+
+ 参数:
+ s (str): 要截断的字符串。
+ max_bytes (int): 字符串的最大字节长度。
+
+ 返回:
+ str: 截断后的字符串。
+ """
+ # 将字符串编码为字节串(默认使用utf-8编码)
+ encoded_str = s.encode('utf-8')
+
+ # 检查字节串的长度
+ if len(encoded_str) > max_bytes:
+ # 截断字节串
+ truncated_bytes = encoded_str[:max_bytes]
+
+ # 确保截断后的字节串是一个有效的UTF-8编码(可能需要移除最后一个字节以形成完整的字符)
+ # 这通过解码然后重新编码来实现,可能会丢失最后一个字符的一部分
+ truncated_str = truncated_bytes.decode('utf-8', 'ignore').encode('utf-8')
+
+ # 返回截断后的字符串(以字节形式编码然后解码回字符串)
+ return truncated_str.decode('utf-8')
+ else:
+ # 如果不需要截断,则返回原始字符串
+ return s
+
+def merge_episodes(dir):
+ """
+ 将漫画从各个章节子文件夹中提取出来, 合并到同一目录, 方便连续阅读
+ 合并前的目录结构: ./comics/漫画标题/章节名/图片
+ 合并后的目录结构: ./comics/漫画标题/图片
+
+ 参数:
+ dir (str): 漫画所在文件夹
+ """
+
+ # 获取目标目录下的所有子文件夹(章节信息),按章节名排序
+ subdirs = sorted([d for d in os.listdir(dir) if os.path.isdir(os.path.join(dir, d))])
+
+ # 存储每个子文件夹中的文件数量
+ counts = []
+
+ # 存储所有文件的完整路径,以便后续处理
+ all_files = []
+
+ # 遍历子文件夹,计算文件数量并收集文件路径
+ for subdir in subdirs:
+ subdir_path = os.path.join(dir, subdir)
+ count = len([f for f in os.listdir(subdir_path) if os.path.isfile(os.path.join(subdir_path, f))])
+ counts.append(count)
+
+ # 获取章节下的所有图片,按图片名排序
+ pics = os.listdir(subdir_path)
+ pics.sort(key=lambda x: str(x.split('.')[0]))
+ for filename in pics:
+ if os.path.isfile(os.path.join(subdir_path, filename)):
+ all_files.append((os.path.join(subdir_path, filename), subdir, count))
+
+ # 确定最大文件数量,用于确定文件名填充宽度
+ max_count = sum(counts) if counts else 0
+ width = len(str(max_count))
+ # 初始化全局文件计数器
+ global_counter = 1
+
+ # 遍历所有文件,复制并重命名
+ for src_file, subdir, count in all_files:
+ # 生成新的文件名,使用宽度进行零填充
+ new_filename = f"{global_counter:0{width}d}{os.path.splitext(src_file)[1]}"
+
+ # 复制文件到目标目录(这里直接复制到dir)
+ shutil.copy2(src_file, os.path.join(dir, new_filename))
+
+ # 更新全局计数器
+ global_counter += 1
+
+ # 删除各个章节的文件夹
+ for subdir in subdirs:
+ shutil.rmtree(os.path.join(dir,subdir))
+
+ print(f"{dir}合并章节完成,共处理了 {global_counter - 1} 个文件")
\ No newline at end of file