From 833ef1698c99485e8a501a0396674d7a04043b9f Mon Sep 17 00:00:00 2001 From: ZayleZou Date: Sun, 12 Apr 2026 01:59:19 +0800 Subject: [PATCH 01/10] =?UTF-8?q?=E4=BF=AE=E8=AE=A2=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E6=97=B6=E4=BB=8D=E4=BC=9A=E5=85=A5=E5=BA=93?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/module/rss/engine.py | 7 ++--- backend/src/test/test_integration.py | 10 +++---- backend/src/test/test_rss_engine_new.py | 35 ++++++++----------------- 3 files changed, 18 insertions(+), 34 deletions(-) diff --git a/backend/src/module/rss/engine.py b/backend/src/module/rss/engine.py index a9dc9dac5..53b27e11b 100644 --- a/backend/src/module/rss/engine.py +++ b/backend/src/module/rss/engine.py @@ -162,14 +162,15 @@ async def refresh_rss(self, client: DownloadClient, rss_id: Optional[int] = None rss_item.last_checked_at = now rss_item.last_error = error self.add(rss_item) + downloaded_torrents = [] for torrent in new_torrents: matched_data = self.match_torrent(torrent) if matched_data: if await client.add_torrent(torrent, matched_data): logger.debug("[Engine] Add torrent %s to client", torrent.name) - torrent.downloaded = True - # Add all torrents to database - self.torrent.add_all(new_torrents) + torrent.downloaded = True + downloaded_torrents.append(torrent) + self.torrent.add_all(downloaded_torrents) self.commit() async def download_bangumi(self, bangumi: Bangumi): diff --git a/backend/src/test/test_integration.py b/backend/src/test/test_integration.py index b9f5ac711..f18be2d28 100644 --- a/backend/src/test/test_integration.py +++ b/backend/src/test/test_integration.py @@ -74,21 +74,17 @@ async def test_full_flow(self, db_engine): # 6. Verify: matched torrents were downloaded assert mock_client.add_torrent.call_count == 2 - # 7. Verify: all torrents stored in DB + # 7. Verify: only downloaded torrents stored in DB (original behavior) all_torrents = engine.torrent.search_all() - assert len(all_torrents) == 3 + assert len(all_torrents) == 2 - # 8. Verify: matched torrents are marked downloaded + # 8. Verify: all stored torrents are marked downloaded downloaded = [t for t in all_torrents if t.downloaded] assert len(downloaded) == 2 # All downloaded torrents should contain "Mushoku Tensei" for t in downloaded: assert "Mushoku Tensei" in t.name - # 9. Verify: unmatched torrent is NOT downloaded - not_downloaded = [t for t in all_torrents if not t.downloaded] - assert len(not_downloaded) == 1 - assert "Unknown Anime" in not_downloaded[0].name async def test_filtered_torrents_not_downloaded(self, db_engine): """Torrents matching the filter regex are NOT downloaded.""" diff --git a/backend/src/test/test_rss_engine_new.py b/backend/src/test/test_rss_engine_new.py index 1a925d3f8..c10907f10 100644 --- a/backend/src/test/test_rss_engine_new.py +++ b/backend/src/test/test_rss_engine_new.py @@ -36,13 +36,15 @@ def clear_bangumi_cache(): class TestPullRss: async def test_returns_only_new_torrents(self, rss_engine): - """pull_rss filters out torrents already in the database.""" + """pull_rss filters out torrents already in the database with downloaded=True.""" rss_item = make_rss_item() rss_engine.rss.add(rss_item) rss_item = rss_engine.rss.search_id(1) - # Pre-insert one torrent into DB - existing = make_torrent(url="https://example.com/existing.torrent", rss_id=1) + # Pre-insert one torrent into DB (must be downloaded=True to be filtered) + existing = make_torrent( + url="https://example.com/existing.torrent", rss_id=1, downloaded=True + ) rss_engine.torrent.add(existing) # Mock _get_torrents to return 3 torrents (1 existing + 2 new) @@ -59,12 +61,14 @@ async def test_returns_only_new_torrents(self, rss_engine): assert all(t.url != "https://example.com/existing.torrent" for t in result) async def test_all_existing_returns_empty(self, rss_engine): - """When all torrents already exist, returns empty list.""" + """When all torrents already exist with downloaded=True, returns empty list.""" rss_item = make_rss_item() rss_engine.rss.add(rss_item) rss_item = rss_engine.rss.search_id(1) - existing = make_torrent(url="https://example.com/only.torrent", rss_id=1) + existing = make_torrent( + url="https://example.com/only.torrent", rss_id=1, downloaded=True + ) rss_engine.torrent.add(existing) with patch.object(RSSEngine, "_get_torrents", new_callable=AsyncMock) as mock_get: @@ -88,6 +92,7 @@ async def test_empty_feed_returns_empty(self, rss_engine): assert result == [] + # --------------------------------------------------------------------------- # match_torrent # --------------------------------------------------------------------------- @@ -217,25 +222,6 @@ async def test_downloads_matched_torrents(self, rss_engine, mock_qb_client): assert len(all_torrents) == 1 assert all_torrents[0].downloaded is True - async def test_unmatched_torrents_stored_not_downloaded(self, rss_engine): - """Unmatched torrents are stored in DB but not marked downloaded.""" - rss_item = make_rss_item(enabled=True) - rss_engine.rss.add(rss_item) - # No bangumi in DB to match - - unmatched = Torrent( - name="[Sub] Unknown Anime - 01 [1080p].mkv", - url="https://example.com/unknown.torrent", - ) - with patch.object(RSSEngine, "_get_torrents", new_callable=AsyncMock) as mock_get: - mock_get.return_value = [unmatched] - client = AsyncMock() - await rss_engine.refresh_rss(client) - - client.add_torrent.assert_not_called() - all_torrents = rss_engine.torrent.search_all() - assert len(all_torrents) == 1 - assert all_torrents[0].downloaded is False async def test_refresh_specific_rss_id(self, rss_engine): """refresh_rss with rss_id only processes that specific feed.""" @@ -258,6 +244,7 @@ async def test_refresh_nonexistent_rss_id(self, rss_engine): client = AsyncMock() await rss_engine.refresh_rss(client, rss_id=999) + mock_get.assert_not_called() From 89d1885615b59368cb4610c5c8baeed202e8d9d8 Mon Sep 17 00:00:00 2001 From: ZayleZou Date: Sun, 12 Apr 2026 01:59:39 +0800 Subject: [PATCH 02/10] =?UTF-8?q?=E4=BF=AE=E8=AE=A2=E5=A4=9A=E4=BD=99?= =?UTF-8?q?=E7=9A=84httpcoredebug=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PROJECT_ANALYSIS.md | 348 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 348 insertions(+) create mode 100644 PROJECT_ANALYSIS.md diff --git a/PROJECT_ANALYSIS.md b/PROJECT_ANALYSIS.md new file mode 100644 index 000000000..6aadb0947 --- /dev/null +++ b/PROJECT_ANALYSIS.md @@ -0,0 +1,348 @@ +# AutoBangumi 项目分析报告 + +**版本**: 3.2.4 | **分析日期**: 2026-04-12 + +## 1. 项目概述 + +AutoBangumi 是一个基于 RSS 的动漫自动下载和整理工具。它监控动漫种子站点(Mikan、DMHY、Nyaa)的 RSS 源,通过 qBittorrent 下载剧集,并将文件整理为 Plex/Jellyfin 兼容的目录结构,支持自动重命名。 + +--- + +## 2. 技术栈 + +| 层级 | 技术 | +|------|------| +| 后端 | Python 3.13 + FastAPI + SQLModel + SQLite | +| 前端 | Vue 3 + TypeScript + Naive UI + Vite + Pinia | +| 包管理 | `uv` (Python), `pnpm` (Node) | +| 部署 | Docker (Alpine 多阶段构建), 端口 7892 | +| CI/CD | GitHub Actions (测试 + Docker 多平台构建 + Telegram 通知) | + +--- + +## 3. 项目结构 + +``` +Auto_Bangumi/ +├── backend/ # Python 后端 (FastAPI + SQLModel) +│ ├── src/ +│ │ ├── main.py # FastAPI 入口, API 挂载在 /api, MCP 挂载在 /mcp +│ │ └── module/ # 核心模块 +│ │ ├── api/ # REST API 路由 +│ │ ├── core/ # 应用控制器, 后台线程, 状态追踪 +│ │ ├── conf/ # 配置管理, 常量, 日志 +│ │ ├── database/ # SQLite 操作 (同步 + 异步引擎, 迁移) +│ │ ├── models/ # SQLModel/Pydantic 数据模型 +│ │ ├── downloader/ # qBittorrent/Aria2 下载客户端抽象 +│ │ ├── manager/ # 文件整理, 重命名, 种子管理 +│ │ ├── mcp/ # Model Context Protocol 服务 +│ │ ├── network/ # HTTP 客户端工具 +│ │ ├── notification/ # 多渠道通知系统 (8个提供者) +│ │ ├── parser/ # 种子名解析, 元数据提取 +│ │ ├── parser/analyser/ # TMDB, Mikan, OpenAI 解析器 +│ │ ├── rss/ # RSS 源解析和分析 +│ │ ├── searcher/ # 种子搜索 (Mikan, DMHY, Nyaa) +│ │ ├── security/ # JWT 认证, WebAuthn/Passkey, IP 白名单 +│ │ ├── update/ # 版本迁移, 数据迁移, 启动例程 +│ │ ├── checker/ # 健康/完整性检查 +│ │ ├── utils/ # JSON 配置, 图片缓存, 数据工具 +│ │ └── ab_decorator/ # 自定义装饰器 +│ └── pyproject.toml +├── webui/ # Vue 3 前端 +│ ├── src/ +│ │ ├── api/ # Axios API 客户端函数 +│ │ ├── components/ # Vue 组件 (basic/, layout/, setting/, setup/) +│ │ ├── pages/ # 路由页面组件 +│ │ ├── store/ # Pinia 状态管理 +│ │ ├── router/ # Vue Router 配置 +│ │ ├── hooks/ # 自定义 Vue 组合式函数 +│ │ ├── i18n/ # 国际化 (zh-CN, en-US) +│ │ ├── services/ # 服务层 (webauthn.ts) +│ │ ├── style/ # 全局 SCSS 样式 +│ │ └── utils/ # 工具函数 +│ └── package.json +├── .github/workflows/ # CI/CD (build.yml) +├── docs/ # VitePress 文档站 +├── scripts/ # 工具脚本 +├── Dockerfile # 多阶段 Docker 构建 +├── entrypoint.sh # Docker 入口 (用户管理 + 应用启动) +├── CLAUDE.md # Claude Code 指导文档 +├── CHANGELOG.md # 详细版本历史 +└── CONTRIBUTING.md # 贡献指南 +``` + +--- + +## 4. 后端依赖 + +### 4.1 生产依赖 + +| 包名 | 版本 | 用途 | +|------|------|------| +| fastapi | >=0.109.0 | Web 框架 / REST API | +| uvicorn | >=0.27.0 | ASGI 服务器 | +| httpx[socks] | >=0.25.0 | 异步 HTTP 客户端 (支持 SOCKS 代理) | +| httpx-socks | >=0.9.0 | SOCKS 代理连接器 | +| beautifulsoup4 | >=4.12.0 | HTML/XML 解析 (RSS 源) | +| sqlmodel | >=0.0.14 | ORM (Pydantic + SQLAlchemy 混合) | +| sqlalchemy[asyncio] | >=2.0.0 | 数据库工具包 (异步支持) | +| aiosqlite | >=0.19.0 | 异步 SQLite 驱动 | +| pydantic | >=2.0.0 | 数据验证 / 设置 | +| python-jose | >=3.3.0 | JWT 令牌创建/验证 | +| passlib | >=1.7.4 | 密码哈希 | +| bcrypt | >=4.0.1,<4.1 | Bcrypt 哈希后端 | +| python-multipart | >=0.0.6 | 表单数据解析 | +| python-dotenv | >=1.0.0 | 环境变量加载 | +| Jinja2 | >=3.1.2 | HTML 模板 (生产 SPA 服务) | +| openai | >=1.54.3 | OpenAI API 集成 (实验性解析器) | +| semver | >=3.0.1 | 语义化版本工具 | +| sse-starlette | >=1.6.5 | Server-Sent Events (MCP) | +| webauthn | >=2.0.0 | WebAuthn/Passkey 认证 | +| urllib3 | >=2.0.3 | HTTP 库 | +| mcp[cli] | >=1.8.0 | Model Context Protocol 服务 SDK | + +### 4.2 开发依赖 + +| 包名 | 版本 | 用途 | +|------|------|------| +| pytest | >=8.0.0 | 测试框架 | +| pytest-asyncio | >=0.23.0 | 异步测试支持 | +| pytest-mock | >=3.12.0 | 测试 Mock | +| ruff | >=0.1.0 | Python Linter | +| black | >=24.0.0 | Python 格式化 | +| pre-commit | >=3.0.0 | 预提交钩子 | + +--- + +## 5. 前端依赖 + +### 5.1 生产依赖 + +| 包名 | 版本 | 用途 | +|------|------|------| +| vue | ^3.5.8 | 核心框架 | +| vue-router | ^4.4.5 | 客户端路由 (Hash 模式) | +| pinia | ^2.2.2 | 状态管理 | +| vue-i18n | ^9.14.0 | 国际化 (zh-CN, en-US) | +| naive-ui | ^2.39.0 | UI 组件库 | +| axios | ^0.27.2 | HTTP 客户端 | +| @vueuse/core | ^10.11.1 | Vue 组合式工具 | +| @headlessui/vue | ^1.7.23 | 无头 UI 组件 | +| vuedraggable | ^4.1.0 | 拖拽列表组件 | + +### 5.2 开发依赖 + +| 包名 | 版本 | 用途 | +|------|------|------| +| vite | ^4.5.5 | 构建工具 / 开发服务器 | +| typescript | ^4.9.5 | 类型检查 | +| unocss | ^0.51.13 | 原子化 CSS 引擎 | +| vitest | ^0.30.1 | 单元测试框架 | +| storybook | ^7.6.20 | 组件开发/文档 | +| vite-plugin-pwa | ^0.16.7 | PWA 支持 | + +--- + +## 6. API 路由 (`/api/v1/`) + +| 路径 | 文件 | 端点功能 | +|------|------|----------| +| `/auth` | auth.py | 登录, 刷新令牌, 登出, 更新 | +| `/passkey` | passkey.py | WebAuthn 注册/认证 | +| `/log` | log.py | 日志访问 | +| `/program` | program.py | 启动/停止/重启/状态控制 | +| `/bangumi` | bangumi.py | 番剧 CRUD, 归档, 偏移检测, 星期管理, TMDB 集成 | +| `/config` | config.py | 配置读写 | +| `/downloader` | downloader.py | qBittorrent 客户端管理 | +| `/rss` | rss.py | RSS CRUD, 刷新, 分析, 订阅 | +| `/search` | search.py | 多提供者种子搜索 | +| `/setup` | setup.py | 首次运行设置向导 | +| `/notification` | notification.py | 通知提供者管理 | + +--- + +## 7. 数据库设计 + +- **引擎**: SQLite (同步 + 异步 via `aiosqlite`) +- **数据路径**: `sqlite:///data/data.db` +- **ORM**: SQLModel (Pydantic v2 + SQLAlchemy) +- **Schema 版本**: 9 (自定义迁移系统) + +### 数据表 + +| 表名 | 说明 | +|------|------| +| `bangumi` | 动漫系列信息 (标题, 年份, 季数, 保存路径, 偏移量, 星期, 海报等) | +| `rssitem` | RSS 源信息 (URL, 解析器, 连接状态, 最后检查时间) | +| `torrent` | 种子记录 (关联 bangumi_id 和 rss_id, 下载状态, qB 哈希) | +| `user` | 用户认证信息 | +| `passkey` | WebAuthn 凭证存储 | +| `schema_version` | 数据库迁移版本追踪 | + +--- + +## 8. 核心后台线程 + +| 线程 | 说明 | 默认间隔 | +|------|------|----------| +| RSSThread | 定时 RSS 解析和种子添加 | 900s (15分钟) | +| RenameThread | 文件重命名循环 | 60s (1分钟) | +| OffsetScanThread | 后台偏移不匹配检测 | 6小时 | +| CalendarRefreshThread | Bangumi 日历数据刷新 | 24小时 | + +--- + +## 9. 通知渠道 (8种) + +telegram, discord, bark, server-chan, wecom, gotify, pushover, webhook + +--- + +## 10. MCP 服务 (`/mcp`) + +### 工具 (10个) + +| 工具名 | 功能 | +|--------|------| +| list_anime | 列出所有动漫 | +| get_anime | 获取单个动漫详情 | +| search_anime | 搜索动漫 | +| subscribe_anime | 订阅动漫 | +| unsubscribe_anime | 取消订阅 | +| list_downloads | 列出下载任务 | +| list_rss_feeds | 列出 RSS 源 | +| get_program_status | 获取程序状态 | +| refresh_feeds | 刷新 RSS 源 | +| update_anime | 更新动漫信息 | + +### 资源 (4个) + +- `anime/list` - 动漫列表 +- `anime/{id}` - 单个动漫 +- `status` - 程序状态 +- `rss/feeds` - RSS 源列表 + +### 安全 + +- 可配置 IP 白名单 (CIDR) +- Bearer Token 认证 +- SSE 传输 (`/mcp/sse`) + +--- + +## 11. 安全与认证 + +| 方式 | 说明 | +|------|------| +| JWT | 令牌通过 HttpOnly Cookie + Bearer Header 发放 (1天有效期) | +| WebAuthn/Passkey | 完整的 Passkey 注册和认证流程 | +| IP 白名单 | 可配置 CIDR 白名单 (登录和 MCP 端点) | +| Bearer Token | 静态令牌可绕过登录 (用于自动化) | +| DEV 模式 | `VERSION == "DEV_VERSION"` 时跳过认证 | + +--- + +## 12. Docker 部署 + +### 构建 + +- **Builder 阶段**: `ghcr.io/astral-sh/uv:0.5-python3.13-alpine` +- **Runtime 阶段**: `python:3.13-alpine` +- 非 root 用户 `ab` (UID 911, GID 911) +- 通过 `tini` 进程管理器启动 + +### 运行 + +```yaml +services: + AutoBangumi: + image: ghcr.io/estrellaxd/auto_bangumi:latest + ports: + - "7892:7892" + volumes: + - ./config:/app/config + - ./data:/app/data + environment: + - TZ=Asia/Shanghai + - PUID=1000 + - PGID=1000 + restart: unless-stopped +``` + +### 卷 + +| 路径 | 用途 | +|------|------| +| `/app/config` | 配置文件 (config.json) | +| `/app/data` | 数据库 + 海报缓存 | + +--- + +## 13. CI/CD 流水线 + +`.github/workflows/build.yml` 包含 6 个 Job: + +1. **test** - Python 测试 (pytest) +2. **webui-test** - 前端类型检查 (pnpm test:build) +3. **version-info** - 版本类型判断 (release/dev/build_test) +4. **build-webui** - Vue SPA 构建 (上传产物) +5. **build-docker** - 多平台 Docker 构建 (amd64 + arm64), 推送 DockerHub + GHCR +6. **release** - 创建 GitHub Release +7. **telegram** - Telegram 发布通知 + +--- + +## 14. 关键架构模式 + +1. **混合 ORM**: SQLModel 同时作为数据库表和 API 请求/响应 Schema +2. **双数据库引擎**: 同步引擎用于常规操作,异步引擎用于 Passkey 操作 +3. **自定义 Schema 迁移**: 轻量级迁移系统 (非 Alembic) +4. **上下文管理器**: DownloadClient、RSSEngine 等使用 `__enter__`/`__exit__` 管理资源生命周期 +5. **多继承组合**: `Program` 类继承所有后台线程能力 +6. **环境变量展开**: 配置支持 `$ENV_VAR` 引用,凭据通过 `@property` 安全展开 +7. **SPA 内嵌服务**: 生产模式下 FastAPI 直接服务 Vue 构建产物 +8. **MCP 集成**: 通过标准 AI 工具接口管理动漫订阅 +9. **可配置安全模型**: IP 白名单 + 静态 Bearer Token + JWT Cookie + WebAuthn 共存 +10. **版本感知启动**: 级联决策树处理首次运行、遗留数据迁移、Schema 迁移 +11. **种子标签**: 下载种子标记 `ab:` 用于重命名阶段关联 +12. **双语响应**: API 响应同时包含 `msg_en` 和 `msg_zh` + +--- + +## 15. 配置系统 + +- **文件**: `config/config.json` (生产) 或 `config/config_dev.json` (开发) +- **环境变量**: `AB_*` 前缀 (如 `AB_DOWNLOADER_HOST`, `AB_INTERVAL_TIME`) +- **配置节**: program, downloader, rss_parser, bangumi_manage, log, proxy, notification, experimental_openai, security +- **凭据安全**: 敏感字段尾部带下划线存储,通过 `@property` 展开 `$VAR` 环境引用 + +--- + +## 16. 开发命令速查 + +### 后端 + +```bash +cd backend && uv sync # 安装依赖 +cd backend && uv run pytest # 运行测试 +cd backend && uv run ruff check src # Lint 检查 +cd backend && uv run black src # 代码格式化 +cd backend/src && uv run python main.py # 启动开发服务器 (端口 7892) +``` + +### 前端 + +```bash +cd webui && pnpm install # 安装依赖 +cd webui && pnpm dev # 启动开发服务器 (端口 5173) +cd webui && pnpm build # 生产构建 +cd webui && pnpm test:build # 类型检查 +cd webui && pnpm lint # Lint 检查 +``` + +### Git 分支规范 + +- `main`: 稳定发布版 +- `X.Y-dev`: 活跃开发分支 (如 `3.2-dev`) +- Bug 修复 → PR 到当前版本 `-dev` 分支 +- 新功能 → PR 到下一版本 `-dev` 分支 From ed8b1c7a2e700206865eb71b2997e501d118e7e1 Mon Sep 17 00:00:00 2001 From: ZayleZou Date: Sun, 12 Apr 2026 01:59:42 +0800 Subject: [PATCH 03/10] =?UTF-8?q?=E4=BF=AE=E8=AE=A2=E5=A4=9A=E4=BD=99?= =?UTF-8?q?=E7=9A=84httpcoredebug=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/module/conf/log.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/module/conf/log.py b/backend/src/module/conf/log.py index 9d9082481..f6f43fad8 100644 --- a/backend/src/module/conf/log.py +++ b/backend/src/module/conf/log.py @@ -48,5 +48,6 @@ def setup_logger(level: int = logging.INFO, reset: bool = False): handlers=[queue_handler], ) - # Suppress verbose HTTP request logs from httpx + # Suppress verbose HTTP request logs from httpx and httpcore logging.getLogger("httpx").setLevel(logging.WARNING) + logging.getLogger("httpcore").setLevel(logging.WARNING) From 484a8dea5d2e379c4edeb6f81b240d462042c9a1 Mon Sep 17 00:00:00 2001 From: ZayleZou Date: Sun, 12 Apr 2026 02:00:00 +0800 Subject: [PATCH 04/10] =?UTF-8?q?=E4=BF=AE=E8=AE=A2RSS=E5=8F=91=E7=8E=B0?= =?UTF-8?q?=E7=9A=84=E7=A7=8D=E5=AD=90=E4=BC=9A=E8=A2=AB=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E8=BF=87=E6=BB=A4=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/module/network/request_contents.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/src/module/network/request_contents.py b/backend/src/module/network/request_contents.py index e271947e7..7730ed73b 100644 --- a/backend/src/module/network/request_contents.py +++ b/backend/src/module/network/request_contents.py @@ -23,10 +23,8 @@ async def get_torrents( if soup: parsed_items = rss_parser(soup) torrents: list[Torrent] = [] - if _filter is None: - _filter = "|".join(settings.rss_parser.filter) for _title, torrent_url, homepage in parsed_items: - if re.search(_filter, _title) is None: + if _filter is None or re.search(_filter, _title) is None: torrents.append( Torrent(name=_title, url=torrent_url, homepage=homepage) ) From d1cb0ba0f097651ee4f6fdc39c818177999c4741 Mon Sep 17 00:00:00 2001 From: Zou Ziyi Date: Sun, 12 Apr 2026 02:27:51 +0800 Subject: [PATCH 05/10] Create docker-image.yml --- .github/workflows/docker-image.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/docker-image.yml diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 000000000..af9e61849 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,18 @@ +name: Docker Image CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Build the Docker image + run: docker buildx build . --file Dockerfile --tag my-image-name:$(date +%s) From d36a1c7e825f30602cb193b5cbb03f5f257bc7df Mon Sep 17 00:00:00 2001 From: ZayleZou Date: Sun, 12 Apr 2026 04:26:59 +0800 Subject: [PATCH 06/10] =?UTF-8?q?=E6=94=AF=E6=8C=81ntfy=E9=80=9A=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/notification/providers/__init__.py | 3 + .../src/module/notification/providers/ntfy.py | 80 +++++++++++++++++++ backend/src/test/test_notification.py | 5 ++ 3 files changed, 88 insertions(+) create mode 100644 backend/src/module/notification/providers/ntfy.py diff --git a/backend/src/module/notification/providers/__init__.py b/backend/src/module/notification/providers/__init__.py index 6dd3b6e85..c46ae35f8 100644 --- a/backend/src/module/notification/providers/__init__.py +++ b/backend/src/module/notification/providers/__init__.py @@ -10,6 +10,7 @@ from module.notification.providers.gotify import GotifyProvider from module.notification.providers.pushover import PushoverProvider from module.notification.providers.webhook import WebhookProvider +from module.notification.providers.ntfy import NtfyProvider if TYPE_CHECKING: from module.notification.base import NotificationProvider @@ -25,6 +26,7 @@ "gotify": GotifyProvider, "pushover": PushoverProvider, "webhook": WebhookProvider, + "ntfy": NtfyProvider, } __all__ = [ @@ -37,4 +39,5 @@ "GotifyProvider", "PushoverProvider", "WebhookProvider", + "NtfyProvider", ] diff --git a/backend/src/module/notification/providers/ntfy.py b/backend/src/module/notification/providers/ntfy.py new file mode 100644 index 000000000..027e24c30 --- /dev/null +++ b/backend/src/module/notification/providers/ntfy.py @@ -0,0 +1,80 @@ +"""ntfy notification provider.""" + +import logging +from typing import TYPE_CHECKING + +from module.models.bangumi import Notification +from module.notification.base import NotificationProvider + +if TYPE_CHECKING: + from module.models.config import NotificationProvider as ProviderConfig + +logger = logging.getLogger(__name__) + + +class NtfyProvider(NotificationProvider): + """ntfy notification provider.""" + + def __init__(self, config: "ProviderConfig"): + super().__init__() + self.server_url = config.server_url.rstrip("/") if config.server_url else "https://ntfy.sh" + self.topic = config.token # ntfy uses topic as the identifier + # For JSON publishing, we POST to the root URL, not the topic URL + self.notification_url = f"{self.server_url}/" + + async def send(self, notification: Notification) -> bool: + """Send notification via ntfy using JSON format.""" + message = self._format_message(notification) + + # Build JSON payload according to ntfy API + data = { + "topic": self.topic, + "title": notification.official_title, + "message": message, + "priority": 3, # default priority + "tags": ["anime", "tv"], + } + + # Add poster as attachment if available + if notification.poster_path and notification.poster_path != "https://mikanani.me": + data["attach"] = notification.poster_path + + try: + resp = await self.post_json_data(self.notification_url, data) + logger.debug("ntfy notification: %s", resp.status_code) + return resp.status_code == 200 + except Exception as e: + logger.warning(f"ntfy notification failed: {e}") + return False + + async def test(self) -> tuple[bool, str]: + """Test ntfy configuration by sending a test message.""" + data = { + "topic": self.topic, + "title": "AutoBangumi 通知测试", + "message": "通知测试成功!\nNotification test successful!", + "priority": 3, + "tags": ["test"], + } + + try: + resp = await self.post_json_data(self.notification_url, data) + if resp.status_code == 200: + return True, "ntfy test message sent successfully" + else: + return False, f"ntfy API returned status {resp.status_code}" + except Exception as e: + return False, f"ntfy test failed: {e}" + + async def post_json_data(self, url: str, data: dict): + """Post JSON data to ntfy.""" + import httpx + + async with httpx.AsyncClient() as client: + response = await client.post( + url, + json=data, + headers={"Content-Type": "application/json"}, + timeout=10.0 + ) + return response \ No newline at end of file diff --git a/backend/src/test/test_notification.py b/backend/src/test/test_notification.py index a9e005b14..81a3f1f58 100644 --- a/backend/src/test/test_notification.py +++ b/backend/src/test/test_notification.py @@ -15,6 +15,7 @@ GotifyProvider, PushoverProvider, WebhookProvider, + NtfyProvider, ) @@ -57,6 +58,10 @@ def test_webhook(self): """Registry contains WebhookProvider for 'webhook' type.""" assert PROVIDER_REGISTRY["webhook"] is WebhookProvider + def test_ntfy(self): + """Registry contains NtfyProvider for 'ntfy' type.""" + assert PROVIDER_REGISTRY["ntfy"] is NtfyProvider + def test_unknown_type(self): """Returns None for unknown notification type.""" result = PROVIDER_REGISTRY.get("unknown_service") From 0b2f8b8bfb8a295c36fe3fe75d3a04592345650b Mon Sep 17 00:00:00 2001 From: ZayleZou Date: Sun, 12 Apr 2026 04:29:31 +0800 Subject: [PATCH 07/10] hint fix --- webui/index.html | 16 +- webui/src/App.vue | 13 +- webui/src/api/__tests__/auth.test.ts | 2 +- webui/src/api/__tests__/bangumi.test.ts | 21 +- webui/src/api/__tests__/rss.test.ts | 7 +- webui/src/api/notification.ts | 2 +- webui/src/components/ab-add-rss.vue | 107 ++++-- webui/src/components/ab-bangumi-card.vue | 26 +- webui/src/components/ab-container.vue | 4 +- webui/src/components/ab-edit-rule.vue | 99 +++-- webui/src/components/ab-fold-panel.vue | 4 +- webui/src/components/ab-search-bar.vue | 12 +- webui/src/components/ab-setting.vue | 1 - webui/src/components/ab-status-bar.vue | 5 +- .../basic/__tests__/ab-button.test.ts | 4 +- .../basic/__tests__/ab-switch.test.ts | 10 +- .../components/basic/ab-adaptive-modal.vue | 2 +- webui/src/components/basic/ab-add.vue | 9 +- .../src/components/basic/ab-bottom-sheet.vue | 28 +- .../basic/ab-button-multi.stories.ts | 42 +-- .../src/components/basic/ab-button-multi.vue | 12 +- webui/src/components/basic/ab-button.vue | 15 +- webui/src/components/basic/ab-checkbox.vue | 2 +- webui/src/components/basic/ab-data-list.vue | 16 +- .../basic/ab-offset-mismatch-dialog.vue | 60 ++- webui/src/components/basic/ab-page-title.vue | 6 +- .../src/components/basic/ab-pull-refresh.vue | 19 +- webui/src/components/basic/ab-search.vue | 7 +- webui/src/components/basic/ab-status.vue | 12 +- .../components/basic/ab-swipe-container.vue | 3 +- webui/src/components/basic/ab-tag.vue | 3 +- webui/src/components/layout/ab-mobile-nav.vue | 39 +- webui/src/components/layout/ab-sidebar.vue | 28 +- webui/src/components/layout/ab-topbar.vue | 21 +- .../components/search/ab-search-confirm.vue | 76 +++- .../components/search/ab-search-filters.vue | 16 +- .../src/components/search/ab-search-modal.vue | 253 +++++++++---- .../setting/config-notification.vue | 45 ++- .../src/components/setting/config-openai.vue | 2 +- .../src/components/setting/config-passkey.vue | 28 +- .../setting/config-search-provider.vue | 31 +- .../src/components/setup/wizard-container.vue | 4 +- .../components/setup/wizard-step-account.vue | 9 +- .../setup/wizard-step-downloader.vue | 21 +- .../setup/wizard-step-notification.vue | 15 +- .../components/setup/wizard-step-review.vue | 27 +- .../src/components/setup/wizard-step-rss.vue | 27 +- webui/src/hooks/__tests__/useApi.test.ts | 14 +- webui/src/hooks/__tests__/useAuth.test.ts | 7 +- webui/src/hooks/useBreakpointQuery.ts | 8 +- webui/src/pages/index.vue | 7 +- webui/src/pages/index/bangumi.vue | 357 ++++++++++-------- webui/src/pages/index/calendar.vue | 105 ++++-- webui/src/pages/index/downloader.vue | 59 ++- webui/src/pages/index/log.vue | 5 +- webui/src/pages/index/player.vue | 36 +- webui/src/pages/login.vue | 17 +- webui/src/services/webauthn.ts | 7 +- webui/src/store/__tests__/bangumi.test.ts | 14 +- webui/src/store/__tests__/rss.test.ts | 36 +- webui/src/store/log.ts | 8 +- webui/src/store/search.ts | 43 ++- webui/src/store/setup.ts | 6 +- webui/src/style/global.scss | 5 +- webui/src/style/transition.scss | 15 +- webui/src/style/var.scss | 71 ++-- webui/types/config.ts | 2 +- webui/unocss.config.ts | 9 +- webui/vitest.config.ts | 7 +- 69 files changed, 1393 insertions(+), 656 deletions(-) diff --git a/webui/index.html b/webui/index.html index 58c562d95..71121fc46 100644 --- a/webui/index.html +++ b/webui/index.html @@ -2,7 +2,10 @@ - + @@ -10,13 +13,18 @@ - + AutoBangumi