Compare commits

..
4 Commits
Author SHA1 Message Date
RRRosmontis 1b2071d78b docs: 更新 README 与线上部署运维说明,新增 .gitignore
- README: 新架构图/端口表/目录结构/双缓存机制/waitress/部署与验证步骤/版本要点
- docs/部署与运维.md: 线上服务器的实际路径、缓存策略、迁移 8 步、日志与变更记录
- .gitignore: 忽略 source/cache/、tokens.json、cookies.txt、__pycache__
2026-09-13 02:26:35 +08:00
RRRosmontis 212adb1e25 perf(pixiv): 改用 waitress 线程池,缓存路径改为相对路径
- IMAGE_DIR 由硬编码 /home/pixiv/images 改为 (Path(__file__).parent/'../cache/pixiv/images').resolve()
  (即 source/cache/pixiv/images),mkdir 补 parents=True
- systemd/pixiv.service: ExecStart 改为 waitress-serve --host=127.0.0.1 --port=9007 --threads=8 pixiv:app
  替代 Flask 开发服务器(thread-per-request 无上限,突发流量下线程爆炸)
- __main__ 兜底加 threaded=True,手动运行时不串行
2026-09-13 02:26:34 +08:00
RRRosmontis 5400dc0a86 feat(bilibili): 新增 BV 本地缓存,修复视频暂存目录泄漏
- 新增 CACHE_DIR(../cache/bilibili),以 BV 号为键缓存 {BV}.mp4,命中即 FileResponse、不再调用 yt-dlp
- 未命中时下载到缓存目录内的隐藏暂存目录 .dl-XXXX,成功后 os.replace 原子落盘
- finally 中 shutil.rmtree(tmpdir):原实现只清理 cookie 临时文件,导致每次取视频都把完整视频
  永久留在 /tmp(无上限磁盘泄漏)
- 淘汰策略:TTL 7 天未访问过期 + 总量超过 2 GiB 按 mtime 从最旧开始删除
- 新增运维接口 GET /bilibili/cache_info(缓存目录/上限/文件数/总量/剩余磁盘)
- 缓存 HIT/MISS/STORE 日志加 flush=True,实时可见
2026-09-13 02:26:34 +08:00
RRRosmontis 99543c9935 refactor: 主入口 main.py 上移到 source/,端口重排为 9008/9007/9006
- main.py: source/openapi/main.py -> source/main.py
- 端口: 聚合层 9888->9008, pixiv 后端 9088->9007, bilibili 后端 19990->9006
- nginx/openapi.rrrosmontis.icu.conf: 三处 proxy_pass 指向 127.0.0.1:9008
- systemd/openapi.service: WorkingDirectory/ExecStart 同步为 source/
2026-09-13 02:26:34 +08:00
7 changed files with 378 additions and 48 deletions
+14
View File
@@ -0,0 +1,14 @@
# 运行时缓存(不入库)
source/cache/
# 凭据与密钥
source/pixiv/tokens.json
source/bilibili/cookies.txt
# Python
__pycache__/
*.pyc
# 杂项
*.swp
.DS_Store
+110 -20
View File
@@ -2,34 +2,124 @@
统一接口服务。线上入口:`https://openapi.rrrosmontis.icu`
聚合层把 **pixiv 图片服务**与 **bilibili 视频服务**收敛到单一对外入口。三个后端进程全部只监听 `127.0.0.1`,对外仅由 nginx 暴露 80/443——即"一个入口、零暴露端口"。
## 架构
```
公网 443/80 nginx (server_name openapi.rrrosmontis.icu)
├── /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
├── /img-master/ /img-original/ -> nginx 直接反代 i.pximg.net(无本地后端)
└── / -> 127.0.0.1:9008(302 跳转文章页)
```
## 端口与进程
| 服务 | 监听 | 入口源码 | 说明 |
|---|---|---|---|
| openapi | `127.0.0.1:9008` | `source/main.py`(aiohttp) | 聚合层,纯转发,**唯一入口** |
| 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) | 视频代理 |
| pixiv-token-refresh | — | `source/pixiv/refresh_token.py` | token 刷新守护(每 30–60 分钟随机) |
| bilibili-refresh | — | `source/bilibili/refresh_cookie.py` | cookie 刷新守护(每 50–70 分钟随机) |
> 历史端口 `9888 / 9088 / 19990` 已于 2026-09-12 废弃,全部改为上述 `900x`。
> pixiv 早期误绑 `0.0.0.0:9088` 对公网裸奔(绕过 nginx 的日志与限流),现已收口为 `127.0.0.1`。
## 结构
```
source/
├── openapi/main.py # 聚合层 (127.0.0.1:9888) /pixiv/get_image /bilibili/get_video
├── main.py # 聚合层 (127.0.0.1:9008),三条路由:/ /pixiv/get_image /bilibili/get_video
├── pixiv/
│ ├── pixiv.py # Pixiv 图片服务 (9088)
│ ├── refresh_token.py # Pixiv OAuth token 刷新守护
│ └── tokens.template.json # Pixiv token 模板
└── bilibili/
├── bilibili.py # bilibili 视频服务 (19990)
├── refresh_cookie.py # cookie 定时刷新守护
├── qr_login.py # 扫码登录获取 cookie
└── qr_login_fast.py
│ ├── pixiv.py # Pixiv 图片服务 (127.0.0.1:9007)
│ ├── refresh_token.py # Pixiv OAuth token 刷新守护
│ └── tokens.template.json # token 模板(实际 tokens.json 不入库)
├── bilibili/
│ ├── bilibili.py # bilibili 视频服务 (127.0.0.1:9006)
│ ├── refresh_cookie.py # cookie 定时刷新守护
│ ├── qr_login.py # 扫码登录获取 cookie
│ └── qr_login_fast.py
└── cache/ # 运行时生成,不入库
├── pixiv/images # 图片缓存
└── bilibili/ # 视频缓存
nginx/
└── openapi.rrrosmontis.icu.conf # nginx 统一入口(/pixiv/ /bilibili/ /img-master/ /img-original/ 图片反代)
└── openapi.rrrosmontis.icu.conf # 统一入口(/pixiv/ /bilibili/ /img-master/ /img-original/)
systemd/
├── openapi.service # 聚合层
├── pixiv.service # Pixiv 图片服务
├── bilibili.service # bilibili 视频服务
├── bilibili-refresh.service # cookie 刷新守护(每 50–70 分钟随机刷新)
└── pixiv-token-refresh.service # Pixiv token 刷新守护(每 30–60 分钟随机刷新)
├── openapi.service # 聚合层
├── pixiv.service # Pixiv 图片服务(waitress --threads=8)
├── bilibili.service # bilibili 视频服务
├── bilibili-refresh.service # cookie 刷新守护
└── pixiv-token-refresh.service # Pixiv token 刷新守护
docs/
└── 部署与运维.md # 线上服务器的路径、缓存、迁移步骤与变更记录
```
## 缓存机制
**pixiv(图片)**:`check_cache()` 以 glob 命中即直接回文件;未命中按 `官方 App API → pixiv.re → pixiv.cat` 三级回退抓取,落盘用 `.tmp` + `os.replace` 原子写。
缓存目录不再硬编码,由模块位置推导:
```python
IMAGE_DIR = (Path(__file__).resolve().parent / "../cache/pixiv/images").resolve()
IMAGE_DIR.mkdir(parents=True, exist_ok=True)
```
即 `source/cache/pixiv/images`;线上常用 `/home/images` 软链指向它,便于访问。
**bilibili(视频)**:以 **BV 号为键**缓存到 `source/cache/bilibili/{BV}.mp4`
- 命中即直接 `FileResponse`,**不再调用 yt-dlp**
- 未命中才下载,且先落到缓存目录内的隐藏暂存目录 `.dl-XXXX`,成功后 `os.replace` 原子落盘
- **无论成功 / 失败 / 超时都会 `shutil.rmtree(tmpdir)`** —— 早期实现只清理 cookie 临时文件、从不清理视频暂存目录,导致每次取视频都把完整视频永久留在 `/tmp`(无上限磁盘泄漏),已修复
- 淘汰策略:单文件 `CACHE_TTL_SECONDS`(默认 7 天)未访问即过期;总量超过 `CACHE_MAX_BYTES`(默认 2 GiB)时按 mtime 从最旧开始删除
- 命中/落盘会在日志打印 `cache HIT / MISS / STORE`(已 `flush=True`,实时可见)
运维接口(只绑本机):
```bash
curl -s 127.0.0.1:9006/bilibili/cache_info
# {cache_dir, cache_max_bytes, cache_ttl_seconds, count, total_bytes, free_bytes, files[]}
```
## 部署
1. token:复制 `source/pixiv/tokens.template.json` 为 `source/pixiv/tokens.json`,填入实际 pixiv token
2. bilibili cookie:放置 `source/bilibili/cookies.txt`(`name=value` 格式),或运行 `qr_login.py` 扫码生成
3. 服务:将 `systemd/*.service` 安装到 `/etc/systemd/system/`,启用并启动 openapi / pixiv / bilibili / bilibili-refresh / pixiv-token-refresh(`systemctl enable --now`)
4. nginx:将 `nginx/openapi.rrrosmontis.icu.conf` 放入 `/etc/nginx/conf.d/`,`nginx -t && systemctl reload nginx`
5. 定时任务:无 cron/timer——bilibili-refresh(每 50–70 分钟刷 cookie)与 pixiv-token-refresh(每 30–60 分钟刷 Pixiv OAuth token,轮换后的 refresh_token 原子落盘)均为常驻守护,内部循环随机间隔,systemd `Restart=always` 兜底
1. **Python 环境**:三服务共用一个环境,需含 `flask`、`waitress`、`aiohttp`、`httpx`、`yt-dlp`
2. **凭据**:
- 复制 `source/pixiv/tokens.template.json` 为 `source/pixiv/tokens.json`(`chmod 600`)并填入实际 pixiv token
- 放置 `source/bilibili/cookies.txt`(`name=value` 格式),或运行 `qr_login.py` 扫码生成
3. **服务**:将 `systemd/*.service` 安装到 `/etc/systemd/system/`,然后
`systemctl daemon-reload && systemctl enable --now openapi pixiv bilibili bilibili-refresh pixiv-token-refresh`
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`
6. **定时任务**:无 cron/timer——两个刷新守护均为常驻循环(内部随机间隔),systemd `Restart=always` 兜底
## 验证
```bash
curl -k "https://openapi.rrrosmontis.icu/pixiv/get_image?id=147113746"
curl -k "https://openapi.rrrosmontis.icu/bilibili/get_video?bv=BV16a41157Q4"
curl -kI "https://openapi.rrrosmontis.icu/bilibili/get_video?bv=BV16a41157Q4" # 只看头部,省流量
curl -s 127.0.0.1:9006/bilibili/cache_info
```
端口暴露自检(三个后端都应"不可达",仅 443/80 通):
```bash
for p in 9006 9007 9008; do timeout 3 bash -c "cat < /dev/null > /dev/tcp/<host>/$p" 2>/dev/null \
&& echo "$p 可连通(异常)" || echo "$p 不可达(正常)"; done
```
## 版本要点
- **2026-09-12**
- 端口重排 `9888/9088/19990 → 9008/9007/9006`,全部仅绑 `127.0.0.1`;pixiv 由 `0.0.0.0:9088` 收口
- 主入口 `main.py` 从 `source/openapi/` 上移到 `source/`,systemd `WorkingDirectory` 同步为 `source/`
- pixiv 缓存路径由硬编码 `/home/pixiv/images` 改为相对 `../cache/pixiv/images`
- pixiv 改由 **waitress 线程池**承载(`--threads=8`),替代 Flask 开发服务器(thread-per-request 无上限)
- bilibili 新增 **BV 真缓存**(`../cache/bilibili`,2 GiB / 7 天淘汰),并修复视频暂存目录永不清理导致的 `/tmp` 泄漏
- 新增 `/bilibili/cache_info` 运维接口;缓存日志加 `flush=True`
- **2026-09-02**:新增 pixiv token 刷新守护;补 `/img-original/` 反代(App API 原图路径)
- **2026-07-31**:三服务迁移落位(Python 环境合并为单一 `general`)
+127
View File
@@ -0,0 +1,127 @@
# 线上部署与运维(hk)
> 记录线上服务器 `hk`(`103.24.218.119:10526`)的实际路径、缓存策略与迁移步骤。
> 架构与代码结构见根目录 [README](../README.md)。
## 对外入口
统一入口:`https://openapi.rrrosmontis.icu`(nginx 监听 80/443)
| 路径 | 链路 |
|---|---|
| `/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) |
| `/img-master/`、`/img-original/` | nginx 直接反代 `i.pximg.net`(无本地后端) |
| `/` | `127.0.0.1:9008`(302 跳转文章页) |
- 其余在服站点:`sqy.bu4.ren`、`xxx.aeolianchi.me`
- 已删除的 vhost:`kim1.work`、`zxx.aeolianchi.me`(2026-09-12)
- 历史域名 `api.bu4.ren` / `pixiv.bu4.ren` / `apixiv.bu4.ren` 已合并进 `openapi.rrrosmontis.icu`
## 进程与端口
三个服务**全部只绑 `127.0.0.1`**,对外仅 nginx 的 443/80。
| systemd 单元 | 源码 | 监听 | 说明 |
|---|---|---|---|
| `openapi.service` | `source/main.py` | `:9008` | 聚合层(aiohttp,纯转发,唯一入口) |
| `pixiv.service` | `source/pixiv/pixiv.py` | `:9007` | Flask,由 waitress(8 线程)承载 |
| `bilibili.service` | `source/bilibili/bilibili.py` | `:9006` | aiohttp + yt-dlp |
| `pixiv-token-refresh.service` | `source/pixiv/refresh_token.py` | — | 常驻刷新 Pixiv OAuth token |
| `bilibili-refresh.service` | `source/bilibili/refresh_cookie.py` | — | 常驻刷新 bilibili cookie |
## 目录结构
```
/home/openapi/
├── source/
│ ├── main.py 聚合层源码(2026-09-12 从 source/openapi/ 上移至此)
│ ├── pixiv/
│ │ ├── pixiv.py 图像代理(Flask)
│ │ ├── refresh_token.py token 刷新守护
│ │ └── tokens.json Pixiv OAuth token(敏感,权限 600,不入库)
│ ├── bilibili/
│ │ ├── bilibili.py 视频代理(aiohttp + yt-dlp)
│ │ ├── cookies.txt bilibili cookie(敏感,不入库)
│ │ ├── refresh_cookie.py cookie 刷新守护
│ │ └── qr_login*.py 扫码登录工具(cookie 失效时人工重登用)
│ └── cache/ 运行时缓存(不入库)
│ ├── pixiv/images pixiv 图片缓存(代码中为 ../cache/pixiv/images)
│ └── bilibili/ bilibili 视频缓存(上限 2 GiB / TTL 7 天)
├── systemd/ 5 个单元文件的归档副本(与 /etc/systemd/system/ 保持一致)
├── nginx/ 宝塔导出的历史存档,非生效路径
├── cert/ SSL 证书
├── env/ Python 环境(general + env_general.tar.gz 整包)
└── 备份说明.txt 本文件的历史版本(旧端口/旧路径,仅供参考)
/home/images -> /home/openapi/source/cache/pixiv/images 便捷软链
```
## 缓存机制
**pixiv(图片)**:`check_cache()` 以 glob 命中即直接回文件;未命中按 `官方 App API → pixiv.re → pixiv.cat` 三级回退抓取,落盘用 `.tmp` + `os.replace` 原子写。
**bilibili(视频)**:以 BV 号为键缓存到 `source/cache/bilibili/{BV}.mp4`
- 命中即复用、不再下载
- 未命中才调 yt-dlp,先下载到缓存目录内的隐藏暂存目录 `.dl-XXXX`,成功后原子 rename 落盘
- 无论成功 / 失败 / 超时都 `rmtree` 该暂存目录
(2026-09-12 修复:原实现从不清理 tmpdir,每次取视频都会把完整视频永久留在 `/tmp`,属无上限磁盘泄漏)
- 淘汰策略:单文件 7 天未访问过期 + 总量超过 2 GiB 时按 mtime 从最旧开始删
- 运维接口:`curl -s 127.0.0.1:9006/bilibili/cache_info`
## nginx
| 文件 | 作用 |
|---|---|
| `/etc/nginx/conf.d/openapi.rrrosmontis.icu.conf` | 统一入口站点(`proxy_pass` → `127.0.0.1:9008`) |
| `/etc/nginx/conf.d/00-websocket.conf` | `$connection_upgrade` map 定义(**必须存在**) |
| `/home/openapi/nginx/**` | 宝塔导出的历史存档,非生效路径 |
## Python 环境
- `/home/openapi/env/general` —— 三服务共用(flask / waitress / aiohttp / httpx / yt-dlp)
- `/home/openapi/env/env_general.tar.gz` —— 环境整包(迁移用)
- 注意:pixiv 现由 **waitress** 承载(`pip install waitress`,纯 Python 线程池),不再使用 Flask 开发服务器
(开发服务器是 thread-per-request 无上限,受控线程池更适合长期运行)
## 迁移到新服务器
1. **环境**:解压 `env/env_general.tar.gz`,直接使用其 `bin/python`(依赖齐全,含 waitress)
2. **源码**:恢复 `source/{main.py,pixiv/,bilibili/}` 到 `/home/openapi/source/`
3. **systemd**:恢复 `systemd/*.service` 到 `/etc/systemd/system/`,`daemon-reload` + `enable` + `start`
4. **nginx**:套用 `conf.d/openapi.rrrosmontis.icu.conf`(并确保 `00-websocket.conf` 的 map 存在)
5. **SSL**:恢复 `cert/<域名>/` 到 `/etc/nginx/cert/<域名>/`
6. **凭据**:放回 `source/pixiv/tokens.json`(600) 与 `source/bilibili/cookies.txt`
7. **软链**:`ln -s /home/openapi/source/cache/pixiv/images /home/images`
8. **验证**:
```bash
curl -k "https://openapi.rrrosmontis.icu/pixiv/get_image?id=xxx"
curl -k "https://openapi.rrrosmontis.icu/bilibili/get_video?bv=BV16a41157Q4"
```
## 日志
```
/var/log/openapi.log /var/log/pixiv.log /var/log/bilibili.log
/var/log/bilibili_refresh.log /var/log/pixiv_token_refresh.log
```
缓存命中/落盘会在 `bilibili.log` 打印 `cache HIT / MISS / STORE`(已加 `flush`,实时可见)。
## 变更记录
**2026-09-12**
- 端口重排 `9888 / 9088 / 19990 → 9008 / 9007 / 9006`(全部仅 `127.0.0.1`)
- pixiv 曾误绑 `0.0.0.0:9088` 对外裸奔(绕过 nginx 日志与限流),已收口为 `127.0.0.1`
- 主入口 `main.py` 由 `source/openapi/` 上移到 `source/`;systemd `WorkingDirectory` 同步
- pixiv 的 `IMAGE_DIR` 由硬编码 `/home/pixiv/images` 改为相对 `../cache/pixiv/images`,并建 `/home/images` 软链
- bilibili 新增真缓存(`../cache/bilibili`)并修复 `/tmp` 泄漏,新增 `cache_info` 接口
- pixiv 改用 waitress 8 线程
- 删除 `kim1.work` / `zxx.aeolianchi.me` 两个 vhost
- 清理历史 `.bak` / `__pycache__` / 空日志等冗余文件
**2026-09-02** 新增 `pixiv-token-refresh`;补 `/img-original/` 反代(App API 原图路径)
**2026-07-31** 三服务迁移落位(Python 环境合并为单一 `general`)
+123 -24
View File
@@ -3,7 +3,9 @@ import sys
import base64
import os
import re
import shutil
import tempfile
import time
from pathlib import Path
import httpx
@@ -12,6 +14,19 @@ from aiohttp import web
COOKIE_FILE = str(Path(__file__).parent / "cookies.txt")
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
# ---------------- 本地缓存(与 pixiv 同级:source/cache/bilibili) ----------------
CACHE_DIR = (Path(__file__).resolve().parent / "../cache/bilibili").resolve()
CACHE_DIR.mkdir(parents=True, exist_ok=True)
CACHE_MAX_BYTES = 2 * 1024 ** 3 # 缓存总量上限 2 GiB
CACHE_TTL_SECONDS = 7 * 24 * 3600 # 7 天未访问即淘汰
VIDEO_EXTS = (".mp4", ".flv", ".mkv", ".webm")
CONTENT_TYPES = {
".mp4": "video/mp4",
".flv": "video/x-flv",
".mkv": "video/x-matroska",
".webm": "video/webm",
}
def _prepare_cookie_file():
"""Convert simple name=value cookies.txt to Netscape format for yt-dlp."""
@@ -36,6 +51,65 @@ def _prepare_cookie_file():
return tmp.name
def _cache_file(bvid):
"""缓存命中则返回文件路径(同时刷新 mtime 作为 LRU 依据),否则 None。"""
for f in CACHE_DIR.glob(f"{bvid}.*"):
if f.is_file() and f.suffix.lower() in VIDEO_EXTS:
try:
os.utime(f, None)
except OSError:
pass
return f
return None
def _cache_files():
out = []
for f in CACHE_DIR.iterdir():
if f.is_file() and f.suffix.lower() in VIDEO_EXTS:
out.append(f)
return out
def _cache_sweep():
"""先按 TTL 淘汰过期文件,再按总量上限从最旧(mtime 最小)开始淘汰。"""
now = time.time()
for f in _cache_files():
try:
if now - f.stat().st_mtime > CACHE_TTL_SECONDS:
f.unlink()
except OSError:
pass
files = _cache_files()
total = 0
for f in files:
try:
total += f.stat().st_size
except OSError:
pass
if total > CACHE_MAX_BYTES:
for f in sorted(files, key=lambda p: p.stat().st_mtime):
if total <= CACHE_MAX_BYTES:
break
try:
size = f.stat().st_size
f.unlink()
total -= size
except OSError:
pass
def _file_response(path, bvid):
return web.FileResponse(
path=path,
headers={
"Content-Type": CONTENT_TYPES.get(path.suffix.lower(), "application/octet-stream"),
"Content-Disposition": f'attachment; filename="{path.name}"',
},
)
async def resolve_bvid(link):
# 直接是 BV 号
m = re.search(r"(BV[a-zA-Z0-9]{10})", link)
@@ -90,11 +164,19 @@ async def get_video(url_b64=None, bv=None):
except ValueError as e:
return web.json_response({"error": str(e)}, status=400)
# 1) 缓存命中:直接回文件,不再调用 yt-dlp
cached = _cache_file(bvid)
if cached:
print(f"[bilibili] cache HIT {bvid} -> {cached.name} ({cached.stat().st_size} B)", flush=True)
return _file_response(cached, bvid)
cookie_tmp = _prepare_cookie_file()
if not cookie_tmp:
return web.json_response({"error": "missing bilibili cookie"}, status=500)
tmpdir = tempfile.mkdtemp(prefix="bilibili_")
print(f"[bilibili] cache MISS {bvid}, downloading...", flush=True)
# 2) 下载到缓存目录内的隐藏临时目录(同盘,才能原子 rename 落盘)
tmpdir = tempfile.mkdtemp(prefix=".dl-", dir=CACHE_DIR)
output_template = os.path.join(tmpdir, "video.%(ext)s")
cmd = [
os.path.join(os.path.dirname(sys.executable), "yt-dlp"),
@@ -113,16 +195,11 @@ async def get_video(url_b64=None, bv=None):
stderr=asyncio.subprocess.PIPE,
)
try:
stdout, stderr = await asyncio.wait_for(
proc.communicate(), timeout=300
)
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=300)
except asyncio.TimeoutError:
proc.kill()
await proc.wait()
return web.json_response({"error": "download timeout"}, status=504)
finally:
if cookie_tmp and os.path.exists(cookie_tmp):
os.unlink(cookie_tmp)
video_files = [p for p in Path(tmpdir).glob("*") if p.is_file()]
if proc.returncode != 0 and not video_files:
@@ -136,26 +213,23 @@ async def get_video(url_b64=None, bv=None):
return web.json_response({"error": "no output file"}, status=502)
video_path = max(video_files, key=lambda p: p.stat().st_size)
ext = video_path.suffix.lower()
content_types = {
".mp4": "video/mp4",
".flv": "video/x-flv",
".mkv": "video/x-matroska",
".webm": "video/webm",
}
content_type = content_types.get(ext, "application/octet-stream")
return web.FileResponse(
path=video_path,
headers={
"Content-Type": content_type,
"Content-Disposition": f'attachment; filename="{video_path.name}"',
},
)
final = CACHE_DIR / f"{bvid}{ext}"
try:
os.replace(video_path, final) # 同盘原子落盘
except OSError:
shutil.move(str(video_path), str(final))
_cache_sweep()
print(f"[bilibili] cache STORE {bvid} -> {final.name} ({final.stat().st_size} B)", flush=True)
return _file_response(final, bvid)
except Exception as e:
print(f"[bilibili] Error: {e}")
return web.json_response({"error": str(e)}, status=500)
finally:
# 关键修复:无论成功/失败/超时都回收临时目录(原实现会把整个视频永久留在 /tmp)
shutil.rmtree(tmpdir, ignore_errors=True)
if cookie_tmp and os.path.exists(cookie_tmp):
os.unlink(cookie_tmp)
async def handle_bilibili(request):
@@ -168,8 +242,33 @@ async def handle_bilibili(request):
return await get_video(url_b64=url_b64)
async def handle_cache_info(request):
files = []
total = 0
for f in _cache_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),
"cache_max_bytes": CACHE_MAX_BYTES,
"cache_ttl_seconds": CACHE_TTL_SECONDS,
"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("/bilibili/get_video", handle_bilibili)
app.router.add_get("/bilibili/cache_info", handle_cache_info)
if __name__ == "__main__":
web.run_app(app, host="127.0.0.1", port=19990)
print(f"Cache directory: {CACHE_DIR}")
web.run_app(app, host="127.0.0.1", port=9006)
+3 -3
View File
@@ -12,8 +12,8 @@ USER_AGENT = "PixivIOSApp/7.13.0 (iOS 14.6; iPhone)"
urllib3.disable_warnings()
IMAGE_DIR = Path("/home/pixiv/images")
IMAGE_DIR.mkdir(exist_ok=True)
IMAGE_DIR = (Path(__file__).resolve().parent / "../cache/pixiv/images").resolve()
IMAGE_DIR.mkdir(parents=True, exist_ok=True)
app = Flask(__name__)
@@ -196,4 +196,4 @@ if __name__ == '__main__':
else:
print("Warning: access_token missing in tokens.json")
print(f"Cache directory: {IMAGE_DIR}")
app.run(host='0.0.0.0', port=9088, debug=False)
app.run(host='127.0.0.1', port=9007, debug=False, threaded=True)
+1 -1
View File
@@ -8,7 +8,7 @@ Type=simple
User=root
WorkingDirectory=/home/openapi/source/pixiv
Environment=PATH=/home/openapi/env/general/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ExecStart=/home/openapi/env/general/bin/python /home/openapi/source/pixiv/pixiv.py
ExecStart=/home/openapi/env/general/bin/waitress-serve --host=127.0.0.1 --port=9007 --threads=8 pixiv:app
Restart=always
RestartSec=5
StandardOutput=append:/var/log/pixiv.log