feat(jm): 新增 JM 漫画下载打包服务(9004,移植自 qqbot jm 模块)
- source/jm/jm.py:aiohttp + jmcomic,GET /jm/get_comic?id=xxx[&range=起-止] 下载至 cache/jm 隐藏暂存目录 → webp 转 jpg 打包 ZIP → HTTP 直返 → 600s 延时删除 单次上限 30 章;per-album 锁防并发;cache_sweep 兜底清理;/jm/cache_info 运维接口 - source/main.py:聚合层注册 /jm/get_comic(新增 JM_BACKEND,复用 proxy()) - systemd/jm.service:独立系统服务(Restart=always,日志 /var/log/jm.log) - README / 部署与运维文档同步更新
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
统一接口服务。线上入口:`https://openapi.rrrosmontis.icu`
|
统一接口服务。线上入口:`https://openapi.rrrosmontis.icu`
|
||||||
|
|
||||||
聚合层把 **pixiv 图片服务**、**bilibili 视频服务**与 **dufe 银杏签到码查询服务**收敛到单一对外入口。四个后端进程全部只监听 `127.0.0.1`,对外仅由 nginx 暴露 80/443——即"一个入口、零暴露端口"。
|
聚合层把 **pixiv 图片服务**、**bilibili 视频服务**、**dufe 银杏签到码查询服务**与 **jm 漫画下载服务**收敛到单一对外入口。五个后端进程全部只监听 `127.0.0.1`,对外仅由 nginx 暴露 80/443——即"一个入口、零暴露端口"。
|
||||||
|
|
||||||
## 架构
|
## 架构
|
||||||
|
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
├── /pixiv/get_image?id=xxx -> 127.0.0.1:9008 main.py 聚合层 -> 127.0.0.1:9007 pixiv
|
├── /pixiv/get_image?id=xxx -> 127.0.0.1:9008 main.py 聚合层 -> 127.0.0.1:9007 pixiv
|
||||||
├── /bilibili/get_video?bv=xxx -> 127.0.0.1:9008 main.py 聚合层 -> 127.0.0.1:9006 bilibili
|
├── /bilibili/get_video?bv=xxx -> 127.0.0.1:9008 main.py 聚合层 -> 127.0.0.1:9006 bilibili
|
||||||
├── /dufe/ginkgostu_sign_in -> 127.0.0.1:9008 main.py 聚合层 -> 127.0.0.1:9005 dufe 签到码查询(GET|POST)
|
├── /dufe/ginkgostu_sign_in -> 127.0.0.1:9008 main.py 聚合层 -> 127.0.0.1:9005 dufe 签到码查询(GET|POST)
|
||||||
|
├── /jm/get_comic?id=xxx -> 127.0.0.1:9008 main.py 聚合层 -> 127.0.0.1:9004 jm
|
||||||
├── /img-master/ /img-original/ -> nginx 直接反代 i.pximg.net(无本地后端)
|
├── /img-master/ /img-original/ -> nginx 直接反代 i.pximg.net(无本地后端)
|
||||||
└── / -> 127.0.0.1:9008(302 跳转文章页)
|
└── / -> 127.0.0.1:9008(302 跳转文章页)
|
||||||
```
|
```
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
| pixiv | `127.0.0.1:9007` | `source/pixiv/pixiv.py`(Flask + waitress) | 图片代理 |
|
| pixiv | `127.0.0.1:9007` | `source/pixiv/pixiv.py`(Flask + waitress) | 图片代理 |
|
||||||
| bilibili | `127.0.0.1:9006` | `source/bilibili/bilibili.py`(aiohttp + yt-dlp) | 视频代理 |
|
| bilibili | `127.0.0.1:9006` | `source/bilibili/bilibili.py`(aiohttp + yt-dlp) | 视频代理 |
|
||||||
| dufe 签到 | `127.0.0.1:9005` | `source/dufe/ginkgostu_sign_in/ginkgo_sign_in.py`(Flask + waitress 16 线程) | 银杏(DUFE)签到码查询,`GET\|POST /dufe/ginkgostu_sign_in` |
|
| dufe 签到 | `127.0.0.1:9005` | `source/dufe/ginkgostu_sign_in/ginkgo_sign_in.py`(Flask + waitress 16 线程) | 银杏(DUFE)签到码查询,`GET\|POST /dufe/ginkgostu_sign_in` |
|
||||||
|
| jm | `127.0.0.1:9004` | `source/jm/jm.py`(aiohttp + jmcomic) | 漫画下载打包(移植自 qqbot jm 模块),`GET /jm/get_comic?id=xxx[&range=起-止]` |
|
||||||
| pixiv-token-refresh | — | `source/pixiv/refresh_token.py` | token 刷新守护(每 30–60 分钟随机) |
|
| pixiv-token-refresh | — | `source/pixiv/refresh_token.py` | token 刷新守护(每 30–60 分钟随机) |
|
||||||
| bilibili-refresh | — | `source/bilibili/refresh_cookie.py` | cookie 刷新守护(每 50–70 分钟随机) |
|
| bilibili-refresh | — | `source/bilibili/refresh_cookie.py` | cookie 刷新守护(每 50–70 分钟随机) |
|
||||||
|
|
||||||
@@ -33,7 +35,7 @@
|
|||||||
|
|
||||||
```
|
```
|
||||||
source/
|
source/
|
||||||
├── main.py # 聚合层 (127.0.0.1:9008),四条路由:/ /pixiv/get_image /bilibili/get_video /dufe/ginkgostu_sign_in
|
├── main.py # 聚合层 (127.0.0.1:9008),五条路由:/ /pixiv/get_image /bilibili/get_video /dufe/ginkgostu_sign_in /jm/get_comic
|
||||||
├── pixiv/
|
├── pixiv/
|
||||||
│ ├── pixiv.py # Pixiv 图片服务 (127.0.0.1:9007)
|
│ ├── pixiv.py # Pixiv 图片服务 (127.0.0.1:9007)
|
||||||
│ ├── refresh_token.py # Pixiv OAuth token 刷新守护
|
│ ├── refresh_token.py # Pixiv OAuth token 刷新守护
|
||||||
@@ -46,18 +48,22 @@ source/
|
|||||||
├── dufe/
|
├── dufe/
|
||||||
│ └── ginkgostu_sign_in/
|
│ └── ginkgostu_sign_in/
|
||||||
│ └── ginkgo_sign_in.py # 银杏(DUFE)签到码查询 (127.0.0.1:9005, GET|POST)
|
│ └── ginkgo_sign_in.py # 银杏(DUFE)签到码查询 (127.0.0.1:9005, GET|POST)
|
||||||
|
├── jm/
|
||||||
|
│ └── jm.py # JM 漫画下载打包服务 (127.0.0.1:9004)
|
||||||
└── cache/ # 运行时生成,不入库
|
└── cache/ # 运行时生成,不入库
|
||||||
├── pixiv/images # 图片缓存
|
├── pixiv/images # 图片缓存
|
||||||
└── bilibili/ # 视频缓存
|
├── bilibili/ # 视频缓存
|
||||||
|
└── jm/ # jm 临时 ZIP(TTL 10 分钟,返回后删除)
|
||||||
nginx/
|
nginx/
|
||||||
└── openapi.rrrosmontis.icu.conf # 统一入口(/pixiv/ /bilibili/ /img-master/ /img-original/)
|
└── openapi.rrrosmontis.icu.conf # 统一入口(/pixiv/ /bilibili/ /img-master/ /img-original/,其余路径由 location / 兜底至聚合层)
|
||||||
systemd/
|
systemd/
|
||||||
├── openapi.service # 聚合层
|
├── openapi.service # 聚合层
|
||||||
├── pixiv.service # Pixiv 图片服务(waitress --threads=8)
|
├── pixiv.service # Pixiv 图片服务(waitress --threads=8)
|
||||||
├── bilibili.service # bilibili 视频服务
|
├── bilibili.service # bilibili 视频服务
|
||||||
├── bilibili-refresh.service # cookie 刷新守护
|
├── bilibili-refresh.service # cookie 刷新守护
|
||||||
├── pixiv-token-refresh.service # Pixiv token 刷新守护
|
├── pixiv-token-refresh.service # Pixiv token 刷新守护
|
||||||
└── ginkgo_sign_in.service # 银杏(DUFE)签到码查询服务
|
├── ginkgo_sign_in.service # 银杏(DUFE)签到码查询服务
|
||||||
|
└── jm.service # JM 漫画下载打包服务
|
||||||
docs/
|
docs/
|
||||||
└── 部署与运维.md # 线上服务器的路径、缓存、迁移步骤与变更记录
|
└── 部署与运维.md # 线上服务器的路径、缓存、迁移步骤与变更记录
|
||||||
```
|
```
|
||||||
@@ -90,14 +96,23 @@ curl -s 127.0.0.1:9006/bilibili/cache_info
|
|||||||
# {cache_dir, cache_max_bytes, cache_ttl_seconds, count, total_bytes, free_bytes, files[]}
|
# {cache_dir, cache_max_bytes, cache_ttl_seconds, count, total_bytes, free_bytes, files[]}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**jm(漫画)**:**非持久缓存**,与 bilibili 不同——ZIP 仅作临时产物
|
||||||
|
|
||||||
|
- 请求 `id`(+ 可选 `range=起-止`,单次 ≤30 章)→ jmcomic 下载到 `cache/jm/` 内隐藏暂存目录 `.dl-<id>-XXXX` → webp 统一转 jpg 打包 ZIP(`{标题}_{id}_ch{起}-{止}.zip`)→ HTTP 返回
|
||||||
|
- 打包完成即 `rmtree` 图片暂存目录;ZIP 在返回后 `ZIP_TTL_SECONDS`(默认 600 秒)延时删除
|
||||||
|
- TTL 窗口内重复请求同一 ZIP 直接复用并重新计时;每次请求附带 `_cache_sweep()` 兜底清理过期 ZIP(防进程重启残留)
|
||||||
|
- 同一本子串行处理(per-album asyncio.Lock),并发请求同本返回 409
|
||||||
|
- 运维接口:`curl -s 127.0.0.1:9004/jm/cache_info`
|
||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
|
|
||||||
1. **Python 环境**:各服务共用一个环境,需含 `flask`、`waitress`、`aiohttp`、`httpx`、`yt-dlp`
|
1. **Python 环境**:各服务共用一个环境,需含 `flask`、`waitress`、`aiohttp`、`httpx`、`yt-dlp`、`jmcomic`、`pillow`
|
||||||
2. **凭据**:
|
2. **凭据**:
|
||||||
- 复制 `source/pixiv/tokens.template.json` 为 `source/pixiv/tokens.json`(`chmod 600`)并填入实际 pixiv token
|
- 复制 `source/pixiv/tokens.template.json` 为 `source/pixiv/tokens.json`(`chmod 600`)并填入实际 pixiv token
|
||||||
- 放置 `source/bilibili/cookies.txt`(`name=value` 格式),或运行 `qr_login.py` 扫码生成
|
- 放置 `source/bilibili/cookies.txt`(`name=value` 格式),或运行 `qr_login.py` 扫码生成
|
||||||
|
- jm 无需凭据,但要求部署机可直连源站(jmcomic 会自动更新 API 域名)
|
||||||
3. **服务**:将 `systemd/*.service` 安装到 `/etc/systemd/system/`,然后
|
3. **服务**:将 `systemd/*.service` 安装到 `/etc/systemd/system/`,然后
|
||||||
`systemctl daemon-reload && systemctl enable --now openapi pixiv bilibili ginkgo_sign_in bilibili-refresh pixiv-token-refresh`
|
`systemctl daemon-reload && systemctl enable --now openapi pixiv bilibili ginkgo_sign_in jm bilibili-refresh pixiv-token-refresh`
|
||||||
4. **nginx**:将 `nginx/openapi.rrrosmontis.icu.conf` 放入 `/etc/nginx/conf.d/`(需存在 `$connection_upgrade` 的 map 定义),`nginx -t && systemctl reload nginx`
|
4. **nginx**:将 `nginx/openapi.rrrosmontis.icu.conf` 放入 `/etc/nginx/conf.d/`(需存在 `$connection_upgrade` 的 map 定义),`nginx -t && systemctl reload nginx`
|
||||||
5. **软链**(可选):`ln -s <部署根>/source/cache/pixiv/images /home/images`
|
5. **软链**(可选):`ln -s <部署根>/source/cache/pixiv/images /home/images`
|
||||||
6. **定时任务**:无 cron/timer——两个刷新守护均为常驻循环(内部随机间隔),systemd `Restart=always` 兜底
|
6. **定时任务**:无 cron/timer——两个刷新守护均为常驻循环(内部随机间隔),systemd `Restart=always` 兜底
|
||||||
@@ -109,18 +124,21 @@ curl -k "https://openapi.rrrosmontis.icu/pixiv/get_image?id=147113746"
|
|||||||
curl -k "https://openapi.rrrosmontis.icu/bilibili/get_video?bv=BV16a41157Q4"
|
curl -k "https://openapi.rrrosmontis.icu/bilibili/get_video?bv=BV16a41157Q4"
|
||||||
curl -kI "https://openapi.rrrosmontis.icu/bilibili/get_video?bv=BV16a41157Q4" # 只看头部,省流量
|
curl -kI "https://openapi.rrrosmontis.icu/bilibili/get_video?bv=BV16a41157Q4" # 只看头部,省流量
|
||||||
curl -k "https://openapi.rrrosmontis.icu/dufe/ginkgostu_sign_in" # 无参应返回 400 JSON
|
curl -k "https://openapi.rrrosmontis.icu/dufe/ginkgostu_sign_in" # 无参应返回 400 JSON
|
||||||
|
curl -k "https://openapi.rrrosmontis.icu/jm/get_comic?id=350234" -o jm.zip # 返回 ZIP
|
||||||
curl -s 127.0.0.1:9006/bilibili/cache_info
|
curl -s 127.0.0.1:9006/bilibili/cache_info
|
||||||
|
curl -s 127.0.0.1:9004/jm/cache_info
|
||||||
```
|
```
|
||||||
|
|
||||||
端口暴露自检(四个后端都应"不可达",仅 443/80 通):
|
端口暴露自检(五个后端都应"不可达",仅 443/80 通):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
for p in 9005 9006 9007 9008; do timeout 3 bash -c "cat < /dev/null > /dev/tcp/<host>/$p" 2>/dev/null \
|
for p in 9004 9005 9006 9007 9008; do timeout 3 bash -c "cat < /dev/null > /dev/tcp/<host>/$p" 2>/dev/null \
|
||||||
&& echo "$p 可连通(异常)" || echo "$p 不可达(正常)"; done
|
&& echo "$p 可连通(异常)" || echo "$p 不可达(正常)"; done
|
||||||
```
|
```
|
||||||
|
|
||||||
## 版本要点
|
## 版本要点
|
||||||
|
|
||||||
|
- **2026-09-23**:新增 **jm 漫画下载打包服务**(`source/jm/jm.py`,aiohttp + jmcomic,`127.0.0.1:9004`,端点 `GET /jm/get_comic?id=xxx[&range=起-止]`),移植自 qqbot 的 jm 模块(去掉 QQ/代理轮换/上传 cf 逻辑,改为 HTTP 直返 ZIP;hk 可直连源站无需代理)。ZIP 为临时产物:返回后 600 秒延时删除,TTL 窗口内同名请求直接复用。聚合层 `main.py` 注册路由(新增 `JM_BACKEND`,复用既有 `proxy()`),nginx 无需改动(`location /` 兜底)
|
||||||
- **2026-09-13**:新增 **dufe 银杏签到码查询服务**(`source/dufe/ginkgostu_sign_in/ginkgo_sign_in.py`,Flask + waitress 16 线程,`127.0.0.1:9005`,端点 `GET|POST /dufe/ginkgostu_sign_in`),并在聚合层 `main.py` 注册该路由——为此新增 `proxy_forward()`(保留请求方法与请求体,原 `proxy()` 保持固定 GET 不变,零改动既有行为)。同期修复该服务两处缺陷:base64 参数补 `urllib.parse.unquote`(兼容 iOS 快捷指令的 `%3D` 编码),`commit` 判定改为 fail-closed(须 `commit.success is True` 才算签到成功)
|
- **2026-09-13**:新增 **dufe 银杏签到码查询服务**(`source/dufe/ginkgostu_sign_in/ginkgo_sign_in.py`,Flask + waitress 16 线程,`127.0.0.1:9005`,端点 `GET|POST /dufe/ginkgostu_sign_in`),并在聚合层 `main.py` 注册该路由——为此新增 `proxy_forward()`(保留请求方法与请求体,原 `proxy()` 保持固定 GET 不变,零改动既有行为)。同期修复该服务两处缺陷:base64 参数补 `urllib.parse.unquote`(兼容 iOS 快捷指令的 `%3D` 编码),`commit` 判定改为 fail-closed(须 `commit.success is True` 才算签到成功)
|
||||||
- **2026-09-12**
|
- **2026-09-12**
|
||||||
- 端口重排 `9888/9088/19990 → 9008/9007/9006`,全部仅绑 `127.0.0.1`;pixiv 由 `0.0.0.0:9088` 收口
|
- 端口重排 `9888/9088/19990 → 9008/9007/9006`,全部仅绑 `127.0.0.1`;pixiv 由 `0.0.0.0:9088` 收口
|
||||||
|
|||||||
+33
-6
@@ -12,6 +12,7 @@
|
|||||||
| `/pixiv/get_image?id=xxx` | `127.0.0.1:9008`(main.py 聚合层)→ `127.0.0.1:9007`(pixiv) |
|
| `/pixiv/get_image?id=xxx` | `127.0.0.1:9008`(main.py 聚合层)→ `127.0.0.1:9007`(pixiv) |
|
||||||
| `/bilibili/get_video?bv=xxx` | `127.0.0.1:9008`(main.py 聚合层)→ `127.0.0.1:9006`(bilibili) |
|
| `/bilibili/get_video?bv=xxx` | `127.0.0.1:9008`(main.py 聚合层)→ `127.0.0.1:9006`(bilibili) |
|
||||||
| `/dufe/ginkgostu_sign_in` | `127.0.0.1:9008`(main.py 聚合层)→ `127.0.0.1:9005`(dufe 签到码查询,支持 GET\|POST) |
|
| `/dufe/ginkgostu_sign_in` | `127.0.0.1:9008`(main.py 聚合层)→ `127.0.0.1:9005`(dufe 签到码查询,支持 GET\|POST) |
|
||||||
|
| `/jm/get_comic?id=xxx[&range=起-止]` | `127.0.0.1:9008`(main.py 聚合层)→ `127.0.0.1:9004`(jm 漫画下载打包) |
|
||||||
| `/img-master/`、`/img-original/` | nginx 直接反代 `i.pximg.net`(无本地后端) |
|
| `/img-master/`、`/img-original/` | nginx 直接反代 `i.pximg.net`(无本地后端) |
|
||||||
| `/` | `127.0.0.1:9008`(302 跳转文章页) |
|
| `/` | `127.0.0.1:9008`(302 跳转文章页) |
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
|
|
||||||
## 进程与端口
|
## 进程与端口
|
||||||
|
|
||||||
四个服务**全部只绑 `127.0.0.1`**,对外仅 nginx 的 443/80。
|
五个服务**全部只绑 `127.0.0.1`**,对外仅 nginx 的 443/80。
|
||||||
|
|
||||||
| systemd 单元 | 源码 | 监听 | 说明 |
|
| systemd 单元 | 源码 | 监听 | 说明 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
| `pixiv.service` | `source/pixiv/pixiv.py` | `:9007` | Flask,由 waitress(8 线程)承载 |
|
| `pixiv.service` | `source/pixiv/pixiv.py` | `:9007` | Flask,由 waitress(8 线程)承载 |
|
||||||
| `bilibili.service` | `source/bilibili/bilibili.py` | `:9006` | aiohttp + yt-dlp |
|
| `bilibili.service` | `source/bilibili/bilibili.py` | `:9006` | aiohttp + yt-dlp |
|
||||||
| `ginkgo_sign_in.service` | `source/dufe/ginkgostu_sign_in/ginkgo_sign_in.py` | `:9005` | 银杏(DUFE)签到码查询,Flask + waitress(16 线程),`GET\|POST` |
|
| `ginkgo_sign_in.service` | `source/dufe/ginkgostu_sign_in/ginkgo_sign_in.py` | `:9005` | 银杏(DUFE)签到码查询,Flask + waitress(16 线程),`GET\|POST` |
|
||||||
|
| `jm.service` | `source/jm/jm.py` | `:9004` | JM 漫画下载打包,aiohttp + jmcomic |
|
||||||
| `pixiv-token-refresh.service` | `source/pixiv/refresh_token.py` | — | 常驻刷新 Pixiv OAuth token |
|
| `pixiv-token-refresh.service` | `source/pixiv/refresh_token.py` | — | 常驻刷新 Pixiv OAuth token |
|
||||||
| `bilibili-refresh.service` | `source/bilibili/refresh_cookie.py` | — | 常驻刷新 bilibili cookie |
|
| `bilibili-refresh.service` | `source/bilibili/refresh_cookie.py` | — | 常驻刷新 bilibili cookie |
|
||||||
|
|
||||||
@@ -50,10 +52,13 @@
|
|||||||
│ ├── dufe/
|
│ ├── dufe/
|
||||||
│ │ └── ginkgostu_sign_in/
|
│ │ └── ginkgostu_sign_in/
|
||||||
│ │ └── ginkgo_sign_in.py 银杏(DUFE)签到码查询服务(127.0.0.1:9005,GET|POST)
|
│ │ └── ginkgo_sign_in.py 银杏(DUFE)签到码查询服务(127.0.0.1:9005,GET|POST)
|
||||||
|
│ ├── jm/
|
||||||
|
│ │ └── jm.py JM 漫画下载打包服务(127.0.0.1:9004)
|
||||||
│ └── cache/ 运行时缓存(不入库)
|
│ └── cache/ 运行时缓存(不入库)
|
||||||
│ ├── pixiv/images pixiv 图片缓存(代码中为 ../cache/pixiv/images)
|
│ ├── pixiv/images pixiv 图片缓存(代码中为 ../cache/pixiv/images)
|
||||||
│ └── bilibili/ bilibili 视频缓存(上限 2 GiB / TTL 7 天)
|
│ ├── bilibili/ bilibili 视频缓存(上限 2 GiB / TTL 7 天)
|
||||||
├── systemd/ 6 个单元文件的归档副本(与 /etc/systemd/system/ 保持一致)
|
│ └── jm/ jm 临时 ZIP(TTL 10 分钟,返回后删除)
|
||||||
|
├── systemd/ 7 个单元文件的归档副本(与 /etc/systemd/system/ 保持一致)
|
||||||
├── nginx/ 宝塔导出的历史存档,非生效路径
|
├── nginx/ 宝塔导出的历史存档,非生效路径
|
||||||
├── cert/ SSL 证书
|
├── cert/ SSL 证书
|
||||||
├── env/ Python 环境(general + env_general.tar.gz 整包)
|
├── env/ Python 环境(general + env_general.tar.gz 整包)
|
||||||
@@ -75,6 +80,16 @@
|
|||||||
- 淘汰策略:单文件 7 天未访问过期 + 总量超过 2 GiB 时按 mtime 从最旧开始删
|
- 淘汰策略:单文件 7 天未访问过期 + 总量超过 2 GiB 时按 mtime 从最旧开始删
|
||||||
- 运维接口:`curl -s 127.0.0.1:9006/bilibili/cache_info`
|
- 运维接口:`curl -s 127.0.0.1:9006/bilibili/cache_info`
|
||||||
|
|
||||||
|
**jm(漫画)**:**非持久临时产物**,移植自 qqbot 的 jm 模块(bj `C:/call/bot/utility/jm.py`)
|
||||||
|
|
||||||
|
- `GET /jm/get_comic?id=xxx[&range=起-止]`:jmcomic 下载 → `cache/jm/` 内隐藏暂存目录 `.dl-<id>-XXXX` → webp 统一转 jpg 打包 ZIP(`{标题}_{id}_ch{起}-{止}.zip`)→ HTTP 直返
|
||||||
|
- 单次上限 30 章(与 qqbot 一致,同时避免超过聚合层 300s 超时);总章节 ≤30 可省略 range 自动全量
|
||||||
|
- 打包完成即 `rmtree` 图片暂存目录;ZIP 在返回后 600 秒(`ZIP_TTL_SECONDS`)延时删除
|
||||||
|
- TTL 窗口内同名 ZIP 重复请求直接复用并 touch 重新计时;每次请求附带 `_cache_sweep()` 兜底清理过期 ZIP(防进程重启残留)
|
||||||
|
- 同一本子串行处理(per-album asyncio.Lock),并发请求同本返回 409
|
||||||
|
- hk 直连源站,无需代理(jmcomic 自动更新 API 域名);qqbot 侧的代理轮换 / 上传 cf / QQ 回复逻辑均未带入
|
||||||
|
- 运维接口:`curl -s 127.0.0.1:9004/jm/cache_info`
|
||||||
|
|
||||||
## nginx
|
## nginx
|
||||||
|
|
||||||
| 文件 | 作用 |
|
| 文件 | 作用 |
|
||||||
@@ -85,15 +100,15 @@
|
|||||||
|
|
||||||
## Python 环境
|
## Python 环境
|
||||||
|
|
||||||
- `/home/openapi/env/general` —— 三服务共用(flask / waitress / aiohttp / httpx / yt-dlp)
|
- `/home/openapi/env/general` —— 五服务共用(flask / waitress / aiohttp / httpx / yt-dlp / jmcomic / pillow)
|
||||||
- `/home/openapi/env/env_general.tar.gz` —— 环境整包(迁移用)
|
- `/home/openapi/env/env_general.tar.gz` —— 环境整包(迁移用)
|
||||||
- 注意:pixiv 现由 **waitress** 承载(`pip install waitress`,纯 Python 线程池),不再使用 Flask 开发服务器
|
- 注意:pixiv 现由 **waitress** 承载(`pip install waitress`,纯 Python 线程池),不再使用 Flask 开发服务器
|
||||||
(开发服务器是 thread-per-request 无上限,受控线程池更适合长期运行)
|
(开发服务器是 thread-per-request 无上限,受控线程池更适合长期运行)
|
||||||
|
|
||||||
## 迁移到新服务器
|
## 迁移到新服务器
|
||||||
|
|
||||||
1. **环境**:解压 `env/env_general.tar.gz`,直接使用其 `bin/python`(依赖齐全,含 waitress)
|
1. **环境**:解压 `env/env_general.tar.gz`,直接使用其 `bin/python`(依赖齐全,含 waitress);若整包过旧需补 `pip install jmcomic`
|
||||||
2. **源码**:恢复 `source/{main.py,pixiv/,bilibili/,dufe/}` 到 `/home/openapi/source/`
|
2. **源码**:恢复 `source/{main.py,pixiv/,bilibili/,dufe/,jm/}` 到 `/home/openapi/source/`
|
||||||
3. **systemd**:恢复 `systemd/*.service` 到 `/etc/systemd/system/`,`daemon-reload` + `enable` + `start`
|
3. **systemd**:恢复 `systemd/*.service` 到 `/etc/systemd/system/`,`daemon-reload` + `enable` + `start`
|
||||||
4. **nginx**:套用 `conf.d/openapi.rrrosmontis.icu.conf`(并确保 `00-websocket.conf` 的 map 存在)
|
4. **nginx**:套用 `conf.d/openapi.rrrosmontis.icu.conf`(并确保 `00-websocket.conf` 的 map 存在)
|
||||||
5. **SSL**:恢复 `cert/<域名>/` 到 `/etc/nginx/cert/<域名>/`
|
5. **SSL**:恢复 `cert/<域名>/` 到 `/etc/nginx/cert/<域名>/`
|
||||||
@@ -104,6 +119,7 @@
|
|||||||
curl -k "https://openapi.rrrosmontis.icu/pixiv/get_image?id=xxx"
|
curl -k "https://openapi.rrrosmontis.icu/pixiv/get_image?id=xxx"
|
||||||
curl -k "https://openapi.rrrosmontis.icu/bilibili/get_video?bv=BV16a41157Q4"
|
curl -k "https://openapi.rrrosmontis.icu/bilibili/get_video?bv=BV16a41157Q4"
|
||||||
curl -k "https://openapi.rrrosmontis.icu/dufe/ginkgostu_sign_in" # 无参应返回 400 JSON
|
curl -k "https://openapi.rrrosmontis.icu/dufe/ginkgostu_sign_in" # 无参应返回 400 JSON
|
||||||
|
curl -k "https://openapi.rrrosmontis.icu/jm/get_comic?id=350234" -o jm.zip # 返回 ZIP
|
||||||
```
|
```
|
||||||
|
|
||||||
## 日志
|
## 日志
|
||||||
@@ -111,12 +127,23 @@
|
|||||||
```
|
```
|
||||||
/var/log/openapi.log /var/log/pixiv.log /var/log/bilibili.log
|
/var/log/openapi.log /var/log/pixiv.log /var/log/bilibili.log
|
||||||
/var/log/ginkgo_sign_in.log /var/log/bilibili_refresh.log /var/log/pixiv_token_refresh.log
|
/var/log/ginkgo_sign_in.log /var/log/bilibili_refresh.log /var/log/pixiv_token_refresh.log
|
||||||
|
/var/log/jm.log
|
||||||
```
|
```
|
||||||
|
|
||||||
缓存命中/落盘会在 `bilibili.log` 打印 `cache HIT / MISS / STORE`(已加 `flush`,实时可见)。
|
缓存命中/落盘会在 `bilibili.log` 打印 `cache HIT / MISS / STORE`(已加 `flush`,实时可见)。
|
||||||
|
|
||||||
## 变更记录
|
## 变更记录
|
||||||
|
|
||||||
|
**2026-09-23**
|
||||||
|
|
||||||
|
- 新增 **jm 漫画下载打包服务**:`source/jm/jm.py` + `jm.service`(`127.0.0.1:9004`,aiohttp + jmcomic,端点 `GET /jm/get_comic?id=xxx[&range=起-止]`)
|
||||||
|
- 移植自 qqbot 的 jm 模块(bj `C:/call/bot/utility/jm.py`):保留章节范围解析 / 文件名清洗 / webp→jpg 打包逻辑;去掉 QQ 回复、代理轮换(hk 可直连源站)、上传 cf 文件服务器三部分,改为 HTTP 直返 ZIP
|
||||||
|
- ZIP 为临时产物:返回后 600 秒延时删除(`asyncio` 延时任务 + 每次请求 `_cache_sweep()` 兜底清理,防进程重启残留);TTL 窗口内同名请求直接复用
|
||||||
|
- 单次上限 30 章(`MAX_CHAPTERS`,与 qqbot 一致;也避免超过聚合层 `proxy()` 的 300s 超时)
|
||||||
|
- 在聚合层 `main.py` 注册该端点:新增 `JM_BACKEND`,复用既有 `proxy()`(固定 GET),零改动既有行为
|
||||||
|
- nginx **无需改动**:`location /` 兜底已把 `/jm/...` 转发给 `127.0.0.1:9008`(与 dufe 相同)
|
||||||
|
- `env/general` 新增依赖 `jmcomic`(2.7.7,`pip install jmcomic`)
|
||||||
|
|
||||||
**2026-09-13**
|
**2026-09-13**
|
||||||
|
|
||||||
- 新增 **dufe 银杏签到码查询服务**:`source/dufe/ginkgostu_sign_in/ginkgo_sign_in.py` + `ginkgo_sign_in.service`(`127.0.0.1:9005`,Flask + waitress 16 线程,端点 `GET|POST /dufe/ginkgostu_sign_in`)
|
- 新增 **dufe 银杏签到码查询服务**:`source/dufe/ginkgostu_sign_in/ginkgo_sign_in.py` + `ginkgo_sign_in.service`(`127.0.0.1:9005`,Flask + waitress 16 线程,端点 `GET|POST /dufe/ginkgostu_sign_in`)
|
||||||
|
|||||||
+280
@@ -0,0 +1,280 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# jm.py — JM 漫画下载 API 模块
|
||||||
|
# 流程: 源站下载 -> 本地临时目录打包 ZIP -> HTTP 返回 -> 延时删除
|
||||||
|
# 移植自 qqbot 的 jm 模块(bj C:/call/bot/utility/jm.py),
|
||||||
|
# 去掉 QQ 机器人 / 代理轮换 / 上传 cf 逻辑,改为与 pixiv/bilibili 一致的 HTTP 直返模式。
|
||||||
|
# hk 服务器可直连源站,无需代理。
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
import unicodedata
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
|
# ---------------- 本地临时缓存(与 pixiv/bilibili 同级:source/cache/jm) ----------------
|
||||||
|
CACHE_DIR = (Path(__file__).resolve().parent / "../cache/jm").resolve()
|
||||||
|
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
MAX_CHAPTERS = 30 # 单次最多章节(与 qqbot 一致,同时避免超过聚合层 300s 超时)
|
||||||
|
ZIP_TTL_SECONDS = 600 # ZIP 返回后保留 10 分钟再删除
|
||||||
|
PORT = 9004
|
||||||
|
|
||||||
|
_image_exts = (".webp", ".jpg", ".png")
|
||||||
|
|
||||||
|
# 同一本子防并发(album_id -> asyncio.Lock)
|
||||||
|
_locks = {}
|
||||||
|
_locks_guard = asyncio.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def _sanitize_filename(name):
|
||||||
|
"""清理文件名非法字符,保留中英文/数字/常见符号"""
|
||||||
|
cleaned = unicodedata.normalize("NFKC", name or "")
|
||||||
|
result = []
|
||||||
|
for ch in cleaned:
|
||||||
|
if ch.isalnum() or ch in ("_", "-", ".", "(", ")", "[", "]", "(", ")", "【", "】"):
|
||||||
|
result.append(ch)
|
||||||
|
elif ch.isspace():
|
||||||
|
result.append("_")
|
||||||
|
else:
|
||||||
|
result.append("-")
|
||||||
|
s = "".join(result)
|
||||||
|
while "__" in s:
|
||||||
|
s = s.replace("__", "_")
|
||||||
|
while "--" in s:
|
||||||
|
s = s.replace("--", "-")
|
||||||
|
return s[:80].strip("_-") or "unnamed"
|
||||||
|
|
||||||
|
|
||||||
|
def _zip_files():
|
||||||
|
return [f for f in CACHE_DIR.glob("*.zip") if f.is_file()]
|
||||||
|
|
||||||
|
|
||||||
|
def _cache_sweep():
|
||||||
|
"""兜底清理:进程重启后残留的过期 ZIP(mtime 超过 TTL 即删)"""
|
||||||
|
now = time.time()
|
||||||
|
for f in _zip_files():
|
||||||
|
try:
|
||||||
|
if now - f.stat().st_mtime > ZIP_TTL_SECONDS:
|
||||||
|
f.unlink()
|
||||||
|
print(f"[jm] sweep expired zip: {f.name}", flush=True)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def _delete_later(path, delay):
|
||||||
|
"""返回响应后延时删除 ZIP"""
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
try:
|
||||||
|
Path(path).unlink(missing_ok=True)
|
||||||
|
print(f"[jm] ttl delete: {Path(path).name}", flush=True)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class _ParamError(RuntimeError):
|
||||||
|
"""参数类错误,返回 400"""
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_album_plan(album_id, range_start, range_end):
|
||||||
|
"""获取元数据并确定章节范围与 ZIP 文件名。
|
||||||
|
返回 (title, episodes, range_start, range_end, zip_name),失败抛异常"""
|
||||||
|
from jmcomic import JmModuleConfig
|
||||||
|
|
||||||
|
option = JmModuleConfig.option_class().default()
|
||||||
|
client = option.build_jm_client()
|
||||||
|
album = client.get_album_detail(int(album_id))
|
||||||
|
title = getattr(album, "title", "") or str(album_id)
|
||||||
|
episodes = getattr(album, "episode_list", []) or []
|
||||||
|
total = len(episodes)
|
||||||
|
if total == 0:
|
||||||
|
raise RuntimeError("未找到任何章节")
|
||||||
|
|
||||||
|
if range_start is None:
|
||||||
|
if total > MAX_CHAPTERS:
|
||||||
|
raise _ParamError(
|
||||||
|
f"共 {total} 章, 超过单次上限 {MAX_CHAPTERS} 章, 请用 range 参数分段下载 (如 range=1-{MAX_CHAPTERS})"
|
||||||
|
)
|
||||||
|
range_start, range_end = 1, total
|
||||||
|
else:
|
||||||
|
if range_start < 1 or range_end < range_start:
|
||||||
|
raise _ParamError("章节范围不合法")
|
||||||
|
if range_end - range_start + 1 > MAX_CHAPTERS:
|
||||||
|
raise _ParamError(f"章节跨度超过 {MAX_CHAPTERS} 章, 请缩小范围")
|
||||||
|
if range_start > total:
|
||||||
|
raise _ParamError(f"起始章节超出总章节数 {total}")
|
||||||
|
range_end = min(range_end, total)
|
||||||
|
|
||||||
|
zip_name = f"{_sanitize_filename(title)}_{album_id}_ch{range_start}-{range_end}.zip"
|
||||||
|
return title, episodes, range_start, range_end, zip_name
|
||||||
|
|
||||||
|
|
||||||
|
def _download_and_pack(album_id, plan):
|
||||||
|
"""下载并打包 ZIP。成功返回 (zip_path, image_count, ok_ch, fail_ch),失败抛异常"""
|
||||||
|
from jmcomic import JmModuleConfig
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
title, episodes, range_start, range_end, zip_name = plan
|
||||||
|
|
||||||
|
work_dir = Path(tempfile.mkdtemp(prefix=f".dl-{album_id}-", dir=CACHE_DIR))
|
||||||
|
try:
|
||||||
|
# 逐章下载(hk 直连源站,无需代理)
|
||||||
|
ok_count = 0
|
||||||
|
fail_count = 0
|
||||||
|
for idx in range(range_start - 1, range_end):
|
||||||
|
photo_id = episodes[idx][0]
|
||||||
|
try:
|
||||||
|
dl_option = JmModuleConfig.option_class().construct({
|
||||||
|
"dir_rule": {"rule": "Bd_Pid", "base_dir": str(work_dir)},
|
||||||
|
"download": {"image": {"suffix": ".webp"}},
|
||||||
|
})
|
||||||
|
dler = JmModuleConfig.downloader_class()(dl_option)
|
||||||
|
dler.download_photo(int(photo_id))
|
||||||
|
ok_count += 1
|
||||||
|
print(f"[jm] 章节下载完成: {album_id}/{photo_id}", flush=True)
|
||||||
|
except Exception as e:
|
||||||
|
fail_count += 1
|
||||||
|
print(f"[jm] 章节下载失败: {album_id}/{photo_id}: {e}", flush=True)
|
||||||
|
|
||||||
|
# 收集图片
|
||||||
|
images = []
|
||||||
|
for ext in _image_exts:
|
||||||
|
images.extend(sorted(work_dir.rglob(f"*{ext}")))
|
||||||
|
if not images:
|
||||||
|
raise RuntimeError("未下载到任何图片")
|
||||||
|
|
||||||
|
# 打包 ZIP (webp 统一转 jpg, 无密码)
|
||||||
|
zip_path = CACHE_DIR / zip_name
|
||||||
|
tmp_dir = Path(tempfile.mkdtemp(prefix=".pack-", dir=CACHE_DIR))
|
||||||
|
try:
|
||||||
|
packed = []
|
||||||
|
for i, p in enumerate(images, 1):
|
||||||
|
out = tmp_dir / f"{i:04d}.jpg"
|
||||||
|
try:
|
||||||
|
img = Image.open(str(p))
|
||||||
|
if img.mode != "RGB":
|
||||||
|
img = img.convert("RGB")
|
||||||
|
img.save(str(out), "JPEG", quality=95)
|
||||||
|
except Exception:
|
||||||
|
out = tmp_dir / f"{i:04d}{p.suffix.lower()}"
|
||||||
|
shutil.copyfile(str(p), str(out))
|
||||||
|
packed.append(out)
|
||||||
|
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
for j in packed:
|
||||||
|
zf.write(j, j.name)
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(tmp_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
print(f"[jm] 打包完成: {zip_name} ({len(images)} 张, 成功 {ok_count} 章 / 失败 {fail_count} 章)", flush=True)
|
||||||
|
return zip_path, len(images), ok_count, fail_count
|
||||||
|
finally:
|
||||||
|
# 无论成功/失败都回收图片临时目录
|
||||||
|
shutil.rmtree(work_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _file_response(zip_path):
|
||||||
|
return web.FileResponse(
|
||||||
|
path=zip_path,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/zip",
|
||||||
|
"Content-Disposition": f'attachment; filename="{zip_path.name}"',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_jm(request):
|
||||||
|
album_id = request.query.get("id", "").strip()
|
||||||
|
if not album_id or not album_id.isdigit():
|
||||||
|
return web.json_response({"error": "missing or invalid id parameter"}, status=400)
|
||||||
|
|
||||||
|
range_start = range_end = None
|
||||||
|
range_str = request.query.get("range", "").strip()
|
||||||
|
if range_str:
|
||||||
|
import re
|
||||||
|
m = re.fullmatch(r"(\d{1,4})-(\d{1,4})", range_str)
|
||||||
|
if not m:
|
||||||
|
return web.json_response(
|
||||||
|
{"error": "invalid range format, expected: start-end (e.g. 1-30)"}, status=400
|
||||||
|
)
|
||||||
|
range_start, range_end = int(m.group(1)), int(m.group(2))
|
||||||
|
|
||||||
|
_cache_sweep()
|
||||||
|
|
||||||
|
# 同本子串行,防止并发重复下载
|
||||||
|
async with _locks_guard:
|
||||||
|
lock = _locks.setdefault(album_id, asyncio.Lock())
|
||||||
|
if lock.locked():
|
||||||
|
return web.json_response(
|
||||||
|
{"error": "same album is being processed, please retry later"}, status=409
|
||||||
|
)
|
||||||
|
|
||||||
|
async with lock:
|
||||||
|
try:
|
||||||
|
plan = await asyncio.to_thread(
|
||||||
|
_resolve_album_plan, album_id, range_start, range_end
|
||||||
|
)
|
||||||
|
except _ParamError as e:
|
||||||
|
return web.json_response({"error": str(e)}, status=400)
|
||||||
|
except Exception as e:
|
||||||
|
return web.json_response(
|
||||||
|
{"error": "failed to fetch album info", "detail": str(e)[:200]}, status=502
|
||||||
|
)
|
||||||
|
|
||||||
|
zip_name = plan[4]
|
||||||
|
existing = CACHE_DIR / zip_name
|
||||||
|
if existing.exists():
|
||||||
|
# TTL 窗口内重复请求:直接复用并重新计时
|
||||||
|
try:
|
||||||
|
existing.touch()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
print(f"[jm] reuse zip: {zip_name}", flush=True)
|
||||||
|
asyncio.create_task(_delete_later(existing, ZIP_TTL_SECONDS))
|
||||||
|
return _file_response(existing)
|
||||||
|
|
||||||
|
try:
|
||||||
|
zip_path, img_count, ok_c, fail_c = await asyncio.to_thread(
|
||||||
|
_download_and_pack, album_id, plan
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return web.json_response(
|
||||||
|
{"error": "download or pack failed", "detail": str(e)[:200]}, status=502
|
||||||
|
)
|
||||||
|
|
||||||
|
asyncio.create_task(_delete_later(zip_path, ZIP_TTL_SECONDS))
|
||||||
|
return _file_response(zip_path)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_cache_info(request):
|
||||||
|
files = []
|
||||||
|
total = 0
|
||||||
|
for f in _zip_files():
|
||||||
|
try:
|
||||||
|
st = f.stat()
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
files.append({"name": f.name, "size": st.st_size, "mtime": int(st.st_mtime)})
|
||||||
|
total += st.st_size
|
||||||
|
return web.json_response(
|
||||||
|
{
|
||||||
|
"cache_dir": str(CACHE_DIR),
|
||||||
|
"zip_ttl_seconds": ZIP_TTL_SECONDS,
|
||||||
|
"max_chapters": MAX_CHAPTERS,
|
||||||
|
"count": len(files),
|
||||||
|
"total_bytes": total,
|
||||||
|
"free_bytes": shutil.disk_usage(CACHE_DIR).free,
|
||||||
|
"files": sorted(files, key=lambda x: -x["size"]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
app = web.Application()
|
||||||
|
app.router.add_get("/jm/get_comic", handle_jm)
|
||||||
|
app.router.add_get("/jm/cache_info", handle_cache_info)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(f"Cache directory: {CACHE_DIR}")
|
||||||
|
web.run_app(app, host="127.0.0.1", port=PORT)
|
||||||
@@ -4,6 +4,7 @@ from aiohttp import web, ClientSession, ClientTimeout
|
|||||||
PIXIV_BACKEND = "http://127.0.0.1:9007"
|
PIXIV_BACKEND = "http://127.0.0.1:9007"
|
||||||
BILIBILI_BACKEND = "http://127.0.0.1:9006"
|
BILIBILI_BACKEND = "http://127.0.0.1:9006"
|
||||||
DUFE_BACKEND = "http://127.0.0.1:9005"
|
DUFE_BACKEND = "http://127.0.0.1:9005"
|
||||||
|
JM_BACKEND = "http://127.0.0.1:9004"
|
||||||
|
|
||||||
|
|
||||||
async def handle_pixiv(request):
|
async def handle_pixiv(request):
|
||||||
@@ -30,6 +31,14 @@ async def handle_dufe_sign_in(request):
|
|||||||
return await proxy_forward(request, target)
|
return await proxy_forward(request, target)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_jm(request):
|
||||||
|
qs = request.query_string
|
||||||
|
target = f"{JM_BACKEND}/jm/get_comic"
|
||||||
|
if qs:
|
||||||
|
target += "?" + qs
|
||||||
|
return await proxy(request, target)
|
||||||
|
|
||||||
|
|
||||||
async def proxy(request, target):
|
async def proxy(request, target):
|
||||||
timeout = ClientTimeout(total=300)
|
timeout = ClientTimeout(total=300)
|
||||||
async with ClientSession(timeout=timeout) as session:
|
async with ClientSession(timeout=timeout) as session:
|
||||||
@@ -92,6 +101,7 @@ app.router.add_get("/pixiv/get_image", handle_pixiv)
|
|||||||
app.router.add_get("/bilibili/get_video", handle_bilibili)
|
app.router.add_get("/bilibili/get_video", handle_bilibili)
|
||||||
app.router.add_get("/dufe/ginkgostu_sign_in", handle_dufe_sign_in)
|
app.router.add_get("/dufe/ginkgostu_sign_in", handle_dufe_sign_in)
|
||||||
app.router.add_post("/dufe/ginkgostu_sign_in", handle_dufe_sign_in)
|
app.router.add_post("/dufe/ginkgostu_sign_in", handle_dufe_sign_in)
|
||||||
|
app.router.add_get("/jm/get_comic", handle_jm)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
web.run_app(app, host="127.0.0.1", port=9008)
|
web.run_app(app, host="127.0.0.1", port=9008)
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=JM Comic Download API Service
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Environment=PATH=/home/openapi/env/general/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=/home/openapi/source/jm
|
||||||
|
ExecStart=/home/openapi/env/general/bin/python /home/openapi/source/jm/jm.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
StandardOutput=append:/var/log/jm.log
|
||||||
|
StandardError=append:/var/log/jm.log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Reference in New Issue
Block a user