From 55c467a5fa8273687ee5c14d443135e5f13b6dde Mon Sep 17 00:00:00 2001 From: ZZzzswszzZZ Date: Mon, 6 Apr 2026 12:38:20 +0800 Subject: [PATCH 01/20] docs: add mikan_parser fix design spec Co-Authored-By: Claude Opus 4.6 --- .../2026-04-06-mikan-parser-fix-design.md | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-06-mikan-parser-fix-design.md diff --git a/docs/superpowers/specs/2026-04-06-mikan-parser-fix-design.md b/docs/superpowers/specs/2026-04-06-mikan-parser-fix-design.md new file mode 100644 index 000000000..ed6636fe8 --- /dev/null +++ b/docs/superpowers/specs/2026-04-06-mikan-parser-fix-design.md @@ -0,0 +1,121 @@ +# mikan_parser 修复设计 + +> 日期: 2026-04-06 +> 上游 spec: .claude/.superpowers/2026-04-06-修复RSS无法添加种子/spec.md (Section 5.1) + +--- + +## 问题 + +`mikan_parser.py` 有 4 个 crash 点,任何网络超时或页面结构变化都会导致 `AttributeError`: +1. `get_html()` 返回 None → `BeautifulSoup(None)` → 后续 `.find()` 返回 None → `.get("style")` crash +2. `soup.find("div", {"class": "bangumi-poster"})` 返回 None → `.get("style")` crash +3. `soup.select_one(...)` 返回 None → `.text` crash +4. 失败结果 `("", "")` 被缓存到 `_mikan_cache` → 永久失败 + +上层 `analyser.py` 捕获 `AttributeError` 后静默 pass,bangumi 以降级标题创建。但因为 `_mikan_cache` 缓存了空值,下次 `rss_loop` 不再重试 → **一次网络超时导致永久降级**。 + +另外,`official_title_parser` 中 `bangumi.official_title = official_title` 会把 raw_parser 设好的降级标题覆盖为空字符串。 + +## 方案 + +### 修改 1:mikan_parser.py — None 安全 + 不缓存失败 + +逐步检查每个可能返回 None 的操作,每个失败点记录含 homepage URL 的 warning 日志。 + +**具体改动:** + +```python +async def mikan_parser(homepage: str) -> tuple[str, str]: + if homepage in _mikan_cache: + return _mikan_cache[homepage] + root_path = parse_url(homepage).host + async with RequestContent() as req: + content = await req.get_html(homepage) + if not content: + logger.warning("[Mikan] Failed to fetch homepage: %s", homepage) + return ("", "") + soup = BeautifulSoup(content, "html.parser") + + poster_link = "" + poster_div = soup.find("div", {"class": "bangumi-poster"}) + if poster_div is None: + logger.warning("[Mikan] No poster div found on: %s", homepage) + else: + poster_style = poster_div.get("style") + if poster_style: + poster_path = poster_style.split("url('")[1].split("')")[0] + poster_path = poster_path.split("?")[0] + img = await req.get_content(f"https://{root_path}{poster_path}") + if img: + suffix = poster_path.split(".")[-1] + poster_link = save_image(img, suffix) + else: + logger.warning("[Mikan] Failed to download poster from: %s", homepage) + else: + logger.warning("[Mikan] Poster div has no style attribute on: %s", homepage) + + official_title = "" + title_elem = soup.select_one('p.bangumi-title a[href^="/Home/Bangumi/"]') + if title_elem is None: + logger.warning("[Mikan] No official title found on: %s", homepage) + else: + official_title = re.sub(r"第.*季", "", title_elem.text).strip() + + # 只缓存成功结果 + if poster_link and official_title: + _mikan_cache[homepage] = (poster_link, official_title) + return (poster_link, official_title) +``` + +### 修改 2:analyser.py — 不覆盖降级值 + +`official_title_parser` 中,只在 mikan_parser 返回非空值时才覆盖 bangumi 的字段: + +```python +async def official_title_parser(self, bangumi, rss, torrent): + if rss.parser == "mikan": + try: + poster_link, official_title = await self.mikan_parser(torrent.homepage) + if official_title: + bangumi.official_title = official_title + if poster_link: + bangumi.poster_link = poster_link + except AttributeError: + logger.warning("[Parser] Mikan torrent has no homepage info.") + elif rss.parser == "tmdb": + ... +``` + +**保留外层 try/except AttributeError** 作为防御层——虽然修复后不应触发,但防御式编程是合理的。 + +## 影响分析 + +### 行为变更对比 + +| 场景 | 修复前 | 修复后 | +|------|--------|--------| +| 网络超时 | crash → catch → bangumi 以 raw_parser 标题创建 → **缓存空值 → 永久降级** | 返回 `("", "")` → bangumi **保留** raw_parser 标题 → **不缓存 → 下次重试** | +| 海报 div 不存在 | crash → 标题也无法提取 | 跳过海报,**继续提取标题** | +| 标题元素不存在 | crash → catch → bangumi 以 raw_parser 标题创建 | 返回空标题 → bangumi **保留** raw_parser 标题 | +| 全部成功 | 正常缓存 | 行为不变 | + +### 上层影响 + +- `analyser.py:official_title_parser` — 不再触发 AttributeError catch,但保留的 catch 不会有副作用 +- `analyser.py:torrents_to_data` — bangumi.official_title 不再被覆盖为空字符串,保留 raw_parser 的降级值 +- `sub_thread.py:rss_loop` — 行为不变,下一次 loop 会重新调用 mikan_parser(因为不缓存了) + +### 其他 Parser + +- `tmdb_parser` — 已有 None 检查,缓存 None 是因为查不到而非临时故障,合理。不需要改。 +- `bgm_parser` — 结构简单,已有 None 检查。不需要改。 + +## 修改清单 + +| 文件 | 改动 | 行数 | +|------|------|------| +| `backend/src/module/parser/analyser/mikan_parser.py` | None 安全 + 不缓存失败 | ~30 行改动 | +| `backend/src/module/rss/analyser.py` | official_title_parser 不覆盖降级值 | ~5 行改动 | + +不涉及数据库、API、前端。 From ce4aa68f05b2c165a4b115d931541db6673c1748 Mon Sep 17 00:00:00 2001 From: ZZzzswszzZZ Date: Mon, 6 Apr 2026 12:41:32 +0800 Subject: [PATCH 02/20] docs: address spec review feedback for mikan_parser fix - Add root_path None check - Add IndexError protection for poster_style parsing - Show complete official_title_parser method with re.sub cleanup - Clarify _mikan_cache growth is out of scope Co-Authored-By: Claude Opus 4.6 --- .../2026-04-06-mikan-parser-fix-design.md | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/docs/superpowers/specs/2026-04-06-mikan-parser-fix-design.md b/docs/superpowers/specs/2026-04-06-mikan-parser-fix-design.md index ed6636fe8..7db708c12 100644 --- a/docs/superpowers/specs/2026-04-06-mikan-parser-fix-design.md +++ b/docs/superpowers/specs/2026-04-06-mikan-parser-fix-design.md @@ -30,6 +30,9 @@ async def mikan_parser(homepage: str) -> tuple[str, str]: if homepage in _mikan_cache: return _mikan_cache[homepage] root_path = parse_url(homepage).host + if not root_path: + logger.warning("[Mikan] Invalid homepage URL: %s", homepage) + return ("", "") async with RequestContent() as req: content = await req.get_html(homepage) if not content: @@ -43,17 +46,20 @@ async def mikan_parser(homepage: str) -> tuple[str, str]: logger.warning("[Mikan] No poster div found on: %s", homepage) else: poster_style = poster_div.get("style") - if poster_style: - poster_path = poster_style.split("url('")[1].split("')")[0] - poster_path = poster_path.split("?")[0] - img = await req.get_content(f"https://{root_path}{poster_path}") - if img: - suffix = poster_path.split(".")[-1] - poster_link = save_image(img, suffix) - else: - logger.warning("[Mikan] Failed to download poster from: %s", homepage) + if poster_style and "url('" in poster_style: + try: + poster_path = poster_style.split("url('")[1].split("')")[0] + poster_path = poster_path.split("?")[0] + img = await req.get_content(f"https://{root_path}{poster_path}") + if img: + suffix = poster_path.rsplit(".", 1)[-1] if "." in poster_path else "jpg" + poster_link = save_image(img, suffix) + else: + logger.warning("[Mikan] Failed to download poster from: %s", homepage) + except (IndexError, ValueError) as e: + logger.warning("[Mikan] Failed to parse poster style on %s: %s", homepage, e) else: - logger.warning("[Mikan] Poster div has no style attribute on: %s", homepage) + logger.warning("[Mikan] Poster div has no style or url() on: %s", homepage) official_title = "" title_elem = soup.select_one('p.bangumi-title a[href^="/Home/Bangumi/"]') @@ -62,18 +68,20 @@ async def mikan_parser(homepage: str) -> tuple[str, str]: else: official_title = re.sub(r"第.*季", "", title_elem.text).strip() - # 只缓存成功结果 + # 只缓存成功结果(不缓存失败,下次 rss_loop 会重试) if poster_link and official_title: _mikan_cache[homepage] = (poster_link, official_title) return (poster_link, official_title) ``` +> 注:`_mikan_cache` 无上限增长的问题不在本次修复范围内(原代码就有),后续可考虑加 LRU。 + ### 修改 2:analyser.py — 不覆盖降级值 `official_title_parser` 中,只在 mikan_parser 返回非空值时才覆盖 bangumi 的字段: ```python -async def official_title_parser(self, bangumi, rss, torrent): +async def official_title_parser(self, bangumi: Bangumi, rss: RSSItem, torrent: Torrent): if rss.parser == "mikan": try: poster_link, official_title = await self.mikan_parser(torrent.homepage) @@ -84,11 +92,24 @@ async def official_title_parser(self, bangumi, rss, torrent): except AttributeError: logger.warning("[Parser] Mikan torrent has no homepage info.") elif rss.parser == "tmdb": - ... + tmdb_title, season, year, poster_link = await self.tmdb_parser( + bangumi.official_title, bangumi.season, settings.rss_parser.language + ) + bangumi.official_title = tmdb_title + bangumi.year = year + bangumi.season = season + bangumi.poster_link = poster_link + else: + pass + # 以下清理逻辑在 if/elif/else 块之外,对所有 parser 路径都生效 + if bangumi.official_title: + bangumi.official_title = re.sub(r"[/:.\\]", " ", bangumi.official_title) ``` **保留外层 try/except AttributeError** 作为防御层——虽然修复后不应触发,但防御式编程是合理的。 +**`else` 分支和 `re.sub` 清理逻辑不变**,mikan 分支的改动不影响 tmdb/else 路径。 + ## 影响分析 ### 行为变更对比 From dea406216a3ec05edc2da6e10cde6e53b1ed27c4 Mon Sep 17 00:00:00 2001 From: ZZzzswszzZZ Date: Mon, 6 Apr 2026 12:49:54 +0800 Subject: [PATCH 03/20] fix(parser): add None safety to mikan_parser and prevent downgrade overwrite mikan_parser crashes on any None value from network failures or page structure changes, and caches failed results permanently. This causes bangumi records to be created with degraded titles that never recover. Changes: - mikan_parser: add None checks for get_html, soup.find, soup.select_one - mikan_parser: add root_path validation and IndexError protection - mikan_parser: only cache successful results (not failures) - analyser: conditional assignment to avoid overwriting raw_parser fallback Co-Authored-By: Claude Opus 4.6 --- .../module/parser/analyser/mikan_parser.py | 56 +++++++++++++------ backend/src/module/rss/analyser.py | 7 ++- 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/backend/src/module/parser/analyser/mikan_parser.py b/backend/src/module/parser/analyser/mikan_parser.py index 109daaaf6..7e4508041 100644 --- a/backend/src/module/parser/analyser/mikan_parser.py +++ b/backend/src/module/parser/analyser/mikan_parser.py @@ -17,26 +17,48 @@ async def mikan_parser(homepage: str): if homepage in _mikan_cache: return _mikan_cache[homepage] root_path = parse_url(homepage).host + if not root_path: + logger.warning("[Mikan] Invalid homepage URL: %s", homepage) + return ("", "") async with RequestContent() as req: content = await req.get_html(homepage) + if not content: + logger.warning("[Mikan] Failed to fetch homepage: %s", homepage) + return ("", "") soup = BeautifulSoup(content, "html.parser") - poster_div = soup.find("div", {"class": "bangumi-poster"}).get("style") - official_title = soup.select_one( - 'p.bangumi-title a[href^="/Home/Bangumi/"]' - ).text - official_title = re.sub(r"第.*季", "", official_title).strip() - if poster_div: - poster_path = poster_div.split("url('")[1].split("')")[0] - poster_path = poster_path.split("?")[0] - img = await req.get_content(f"https://{root_path}{poster_path}") - suffix = poster_path.split(".")[-1] - poster_link = save_image(img, suffix) - result = (poster_link, official_title) - _mikan_cache[homepage] = result - return result - result = ("", "") - _mikan_cache[homepage] = result - return result + + poster_link = "" + poster_div = soup.find("div", {"class": "bangumi-poster"}) + if poster_div is None: + logger.warning("[Mikan] No poster div found on: %s", homepage) + else: + poster_style = poster_div.get("style") + if poster_style and "url('" in poster_style: + try: + poster_path = poster_style.split("url('")[1].split("')")[0] + poster_path = poster_path.split("?")[0] + img = await req.get_content(f"https://{root_path}{poster_path}") + if img: + suffix = poster_path.rsplit(".", 1)[-1] if "." in poster_path else "jpg" + poster_link = save_image(img, suffix) + else: + logger.warning("[Mikan] Failed to download poster from: %s", homepage) + except (IndexError, ValueError) as e: + logger.warning("[Mikan] Failed to parse poster style on %s: %s", homepage, e) + else: + logger.warning("[Mikan] Poster div has no style or url() on: %s", homepage) + + official_title = "" + title_elem = soup.select_one('p.bangumi-title a[href^="/Home/Bangumi/"]') + if title_elem is None: + logger.warning("[Mikan] No official title found on: %s", homepage) + else: + official_title = re.sub(r"第.*季", "", title_elem.text).strip() + + # 只缓存成功结果(失败不缓存,下次 rss_loop 会重试) + if poster_link and official_title: + _mikan_cache[homepage] = (poster_link, official_title) + return (poster_link, official_title) if __name__ == '__main__': diff --git a/backend/src/module/rss/analyser.py b/backend/src/module/rss/analyser.py index cbd7a9afd..0bb8cdb97 100644 --- a/backend/src/module/rss/analyser.py +++ b/backend/src/module/rss/analyser.py @@ -15,12 +15,15 @@ class RSSAnalyser(TitleParser): async def official_title_parser(self, bangumi: Bangumi, rss: RSSItem, torrent: Torrent): if rss.parser == "mikan": try: - bangumi.poster_link, bangumi.official_title = await self.mikan_parser( + poster_link, official_title = await self.mikan_parser( torrent.homepage ) + if official_title: + bangumi.official_title = official_title + if poster_link: + bangumi.poster_link = poster_link except AttributeError: logger.warning("[Parser] Mikan torrent has no homepage info.") - pass elif rss.parser == "tmdb": tmdb_title, season, year, poster_link = await self.tmdb_parser( bangumi.official_title, bangumi.season, settings.rss_parser.language From 7f413d5828f6c8e3ff636d16165b0c7e2f96fe94 Mon Sep 17 00:00:00 2001 From: ZZzzswszzZZ Date: Mon, 6 Apr 2026 13:57:26 +0800 Subject: [PATCH 04/20] docs: add torrent management design spec Co-Authored-By: Claude Opus 4.6 --- .../2026-04-06-torrent-management-design.md | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-06-torrent-management-design.md diff --git a/docs/superpowers/specs/2026-04-06-torrent-management-design.md b/docs/superpowers/specs/2026-04-06-torrent-management-design.md new file mode 100644 index 000000000..da1756969 --- /dev/null +++ b/docs/superpowers/specs/2026-04-06-torrent-management-design.md @@ -0,0 +1,139 @@ +# 种子管理入口设计 + +> 日期: 2026-04-06 +> 上游 spec: .claude/.superpowers/2026-04-06-修复RSS无法添加种子/spec.md (Section 5.2) + +--- + +## 背景 + +torrent 表是高频故障表(8 个相关 issue 中 4 个直接涉及)。当前 84% 的种子记录(353/419)是孤儿——没有关联的 bangumi 记录。用户无法查看或清理这些数据。 + +核心问题: +- 无法查看某番剧下有哪些种子 +- 无法删除单条种子记录(让 refresh_rss 重新处理) +- 无法清理孤儿种子 + +## 方案 + +### 后端 API 设计 + +采用混合策略:正常番剧的种子管理挂在 bangumi 接口下,孤儿种子用独立端点。 + +#### 新增端点 + +| 端点 | 方法 | 说明 | +|------|------|------| +| `/api/v1/bangumi/{id}/torrents` | GET | 获取某番剧下的种子列表 | +| `/api/v1/bangumi/{id}/torrents` | DELETE | 清空该番剧下所有种子 | +| `/api/v1/bangumi/{id}/torrents/{torrent_id}` | DELETE | 删除单条种子 | +| `/api/v1/bangumi/torrents/orphans` | GET | 获取孤儿种子列表 | +| `/api/v1/bangumi/torrents/orphans` | DELETE | 清理所有孤儿种子 | + +#### 为什么不用 id=-1 复用? + +删除 bangumi 时有级联逻辑(删 RSS → 删种子 → 删 bangumi 记录 → 可选删文件)。要复用 `DELETE /bangumi/delete/{id}` 就得大改 delete_rule,风险高且不必要。独立端点更清晰。 + +#### 为什么种子端点挂在 bangumi 下而不是顶层 /torrents/? + +种子的消费场景是"番剧管理"——用户查看某部番的种子、清理某部番的种子。挂在 bangumi 下符合用户心智模型。 + +### 数据库层(`torrent.py`) + +新增方法: + +```python +def search_by_bangumi_id(self, bangumi_id: int) -> list[Torrent]: + """查询某番剧下的所有种子""" + +def search_orphans(self) -> list[Torrent]: + """查询 bangumi_id 为 NULL 的孤儿种子""" + +def delete_one(self, torrent_id: int) -> bool: + """删除单条种子记录,返回是否成功""" + +def delete_orphans(self) -> int: + """删除所有孤儿种子,返回删除数量""" +``` + +已有 `delete_by_bangumi_id(bangumi_id)` 可直接复用于清空某番下所有种子。 + +### 前端设计 + +#### 番剧列表页 Others 卡片 + +在番剧列表末尾添加一个特殊的 "Others" 卡片: +- 复用现有番剧卡片组件的视觉结构 +- 标题:"Others" / "未匹配种子" +- 副标题:显示孤儿种子数量 badge +- 无海报,使用占位图标 +- 点击后展开/跳转到种子列表 + +#### 种子列表 + +每个种子项显示: +- 种子名称 +- 匹配状态(已匹配 / 孤儿) +- 下载状态(downloaded badge) +- 来源(有 rss_id → "RSS";无 rss_id → "手动/订阅") +- 删除按钮 + +底部有"清理所有"按钮,带确认对话框。 + +#### 前端 API 调用(`webui/src/api/bangumi.ts` 新增) + +```typescript +getTorrents(bangumiId: number): Promise +deleteTorrent(bangumiId: number, torrentId: number): Promise +deleteAllTorrents(bangumiId: number): Promise +getOrphanTorrents(): Promise +deleteOrphanTorrents(): Promise +``` + +#### i18n 新增 key + +``` +bangumi.others.title — "Others" / "未匹配种子" +bangumi.torrents.delete — "Delete" / "删除" +bangumi.torrents.deleteAll — "Delete All" / "清空所有" +bangumi.torrents.confirmDelete — "Are you sure?" / "确认删除?" +bangumi.torrents.orphanCount — "{count} orphan torrents" / "{count} 条未匹配种子" +bangumi.torrents.source.rss — "RSS" / "RSS" +bangumi.torrents.source.manual — "Manual" / "手动/订阅" +``` + +### 删除行为说明 + +所有删除操作**只删数据库记录,不删下载器中的实际文件**。 + +删除后效果: +- `refresh_rss` 的 `check_new` 不再过滤这些种子 URL +- 种子被重新拉取和处理(如果还在 RSS feed 中) +- 这是用户"重试"的主要方式 + +### 诊断功能 + +不需要独立入口。种子列表本身就是最好的诊断 UI: + +| 信息 | 展示方式 | +|------|---------| +| 匹配状态 | 种子列表中显示 bangumi_id 是否存在 | +| 下载状态 | downloaded 字段的 badge | +| 来源 | rss_id 是否存在 | +| 匹配失败原因 | 问题 1 的增强日志已覆盖 | + +## 修改清单 + +| 文件 | 改动 | +|------|------| +| `backend/src/module/database/torrent.py` | 新增 search_by_bangumi_id, search_orphans, delete_one, delete_orphans | +| `backend/src/module/api/bangumi.py` | 新增 5 个端点 | +| `webui/src/api/bangumi.ts` | 新增 5 个 API 调用函数 | +| `webui/src/pages/` 或 `webui/src/components/` | Others 卡片 + 种子列表组件 | +| `webui/src/i18n/` | 新增翻译 key | + +## 不涉及 + +- 不修改现有 delete_rule 的级联逻辑 +- 不修改 refresh_rss 的匹配逻辑 +- 不创建实际的 "Others" bangumi 数据库记录 From 7d73f2c6a151e54be3f5568e83498b6cab96f816 Mon Sep 17 00:00:00 2001 From: ZZzzswszzZZ Date: Mon, 6 Apr 2026 14:03:35 +0800 Subject: [PATCH 05/20] docs: address spec review feedback for torrent management - Fix route conflict: orphans must register before {id}/torrents - Add torrent ownership check for single delete endpoint - Expand Torrent type with bangumi_id/rss_id fields - Define API response format for all endpoints - Clarify Others card uses route navigation, not inline Co-Authored-By: Claude Opus 4.6 --- .../2026-04-06-torrent-management-design.md | 68 ++++++++++++++++--- 1 file changed, 59 insertions(+), 9 deletions(-) diff --git a/docs/superpowers/specs/2026-04-06-torrent-management-design.md b/docs/superpowers/specs/2026-04-06-torrent-management-design.md index da1756969..8d324f140 100644 --- a/docs/superpowers/specs/2026-04-06-torrent-management-design.md +++ b/docs/superpowers/specs/2026-04-06-torrent-management-design.md @@ -22,13 +22,16 @@ torrent 表是高频故障表(8 个相关 issue 中 4 个直接涉及)。当 #### 新增端点 +> **路由注册顺序**:orphans 端点必须在 `{id}/torrents` 之前注册。 +> FastAPI 按注册顺序匹配,如果 `{id}/torrents` 先注册,`"torrents"` 会被当作 `id` 参数导致 404。 + | 端点 | 方法 | 说明 | |------|------|------| +| `/api/v1/bangumi/torrents/orphans` | GET | 获取孤儿种子列表 | +| `/api/v1/bangumi/torrents/orphans` | DELETE | 清理所有孤儿种子 | | `/api/v1/bangumi/{id}/torrents` | GET | 获取某番剧下的种子列表 | | `/api/v1/bangumi/{id}/torrents` | DELETE | 清空该番剧下所有种子 | | `/api/v1/bangumi/{id}/torrents/{torrent_id}` | DELETE | 删除单条种子 | -| `/api/v1/bangumi/torrents/orphans` | GET | 获取孤儿种子列表 | -| `/api/v1/bangumi/torrents/orphans` | DELETE | 清理所有孤儿种子 | #### 为什么不用 id=-1 复用? @@ -47,15 +50,19 @@ def search_by_bangumi_id(self, bangumi_id: int) -> list[Torrent]: """查询某番剧下的所有种子""" def search_orphans(self) -> list[Torrent]: - """查询 bangumi_id 为 NULL 的孤儿种子""" + """查询 bangumi_id IS NULL 的孤儿种子""" def delete_one(self, torrent_id: int) -> bool: - """删除单条种子记录,返回是否成功""" + """删除单条种子记录,返回是否成功。 + torrent_id 不存在时返回 False。""" def delete_orphans(self) -> int: """删除所有孤儿种子,返回删除数量""" ``` +> **归属校验**:`DELETE /{id}/torrents/{torrent_id}` 需要校验 `torrent.bangumi_id == id`, +> 防止通过一个番剧的端点删除另一个番剧的种子。不匹配时返回 404。 + 已有 `delete_by_bangumi_id(bangumi_id)` 可直接复用于清空某番下所有种子。 ### 前端设计 @@ -67,9 +74,11 @@ def delete_orphans(self) -> int: - 标题:"Others" / "未匹配种子" - 副标题:显示孤儿种子数量 badge - 无海报,使用占位图标 -- 点击后展开/跳转到种子列表 +- 点击后**路由跳转**到 `/bangumi/others/torrents`(独立页面,非内联展开) + +#### 种子列表页(`/bangumi/others/torrents` 或 `/bangumi/{id}/torrents`) -#### 种子列表 +独立路由页面,复用同一个种子列表组件。 每个种子项显示: - 种子名称 @@ -80,6 +89,8 @@ def delete_orphans(self) -> int: 底部有"清理所有"按钮,带确认对话框。 +> **不需要分页**——当前最多几百条种子记录,一次性加载即可。如果未来数据量增大再加。 + #### 前端 API 调用(`webui/src/api/bangumi.ts` 新增) ```typescript @@ -90,6 +101,31 @@ getOrphanTorrents(): Promise deleteOrphanTorrents(): Promise ``` +#### 前端 Torrent 类型扩展(`webui/types/torrent.ts`) + +现有 `Torrent` 接口缺少 `bangumi_id` 和 `rss_id` 字段,需要补充: + +```typescript +export interface Torrent { + id: number; + name: string; + url: string; + homepage: string | null; + downloaded: boolean; + bangumi_id: number | null; // 新增:匹配状态 + rss_id: number | null; // 新增:来源 + qb_hash: string | null; +} +``` + +#### API 响应格式 + +所有 API 使用项目现有的 `ResponseModel` 统一格式: +- 列表端点:`{"status": true, "data": [...]}` +- 删除端点:`{"status": true, "msg_en": "...", "msg_zh": "Deleted N records"}` +- 资源不存在:`{"status": false, "status_code": 404, "msg_en": "...", "msg_zh": "..."}` +- 无孤儿种子时:返回空列表 `[]`,不报错 + #### i18n 新增 key ``` @@ -127,13 +163,27 @@ bangumi.torrents.source.manual — "Manual" / "手动/订阅" | 文件 | 改动 | |------|------| | `backend/src/module/database/torrent.py` | 新增 search_by_bangumi_id, search_orphans, delete_one, delete_orphans | -| `backend/src/module/api/bangumi.py` | 新增 5 个端点 | +| `backend/src/module/api/bangumi.py` | 新增 5 个端点(orphans 在前,{id} 在后) | | `webui/src/api/bangumi.ts` | 新增 5 个 API 调用函数 | -| `webui/src/pages/` 或 `webui/src/components/` | Others 卡片 + 种子列表组件 | +| `webui/src/types/torrent.ts` | 扩展 Torrent 接口,添加 bangumi_id, rss_id | +| `webui/src/pages/` 或 `webui/src/components/` | Others 卡片 + 种子列表页组件 | | `webui/src/i18n/` | 新增翻译 key | +| `webui/src/router/` | 新增 /bangumi/others/torrents 路由 | ## 不涉及 -- 不修改现有 delete_rule 的级联逻辑 +- 不修改现有 delete_rule 的级联逻辑(删番剧仍自动删种子) - 不修改 refresh_rss 的匹配逻辑 - 不创建实际的 "Others" bangumi 数据库记录 +- 删除种子时不删下载器中的实际文件 +- 删除种子时不删关联的 bangumi 记录 + +## 路由注册顺序 + +FastAPI 按注册顺序匹配。orphans 端点**必须**在 `{id}/torrents` 之前注册,否则 FastAPI 会把字符串 `"torrents"` 解析为 `{id}` 参数: + +```python +# 正确顺序 +router.get("/torrents/orphans", ...) # 先注册 +router.get("/{id}/torrents", ...) # 后注册 +``` From 5914c20f07307a20e4f4766d933f569f3c79fbde Mon Sep 17 00:00:00 2001 From: ZZzzswszzZZ Date: Mon, 6 Apr 2026 14:14:33 +0800 Subject: [PATCH 06/20] docs: add torrent management implementation plan Co-Authored-By: Claude Opus 4.6 --- .../plans/2026-04-06-torrent-management.md | 713 ++++++++++++++++++ 1 file changed, 713 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-06-torrent-management.md diff --git a/docs/superpowers/plans/2026-04-06-torrent-management.md b/docs/superpowers/plans/2026-04-06-torrent-management.md new file mode 100644 index 000000000..1438ee87f --- /dev/null +++ b/docs/superpowers/plans/2026-04-06-torrent-management.md @@ -0,0 +1,713 @@ +# 种子管理入口 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 为番剧管理增加种子查看和删除能力,包括一个虚拟的 "Others" 入口展示孤儿种子。 + +**Architecture:** 后端在 bangumi API 下新增 6 个端点(orphans 在前避免路由冲突),数据库层新增 4 个方法。前端使用 file-based routing 创建种子列表页,复用一个 `ab-torrent-list` 组件。Others 作为虚拟 bangumi 卡片插入番剧列表。 + +**Tech Stack:** FastAPI, SQLModel/SQLite, Vue 3 + TypeScript, unplugin-vue-router, UnoCSS + +--- + +## File Structure + +| Action | File | Responsibility | +|--------|------|----------------| +| Modify | `backend/src/module/database/torrent.py` | 新增 search_by_bangumi_id, search_orphans, delete_one, delete_orphans | +| Modify | `backend/src/module/api/bangumi.py` | 新增 6 个端点(import 加 Torrent, ResponseModel) | +| Modify | `webui/types/torrent.ts` | 扩展 Torrent 接口 | +| Modify | `webui/src/api/bangumi.ts` | 新增 6 个 API 函数 | +| Create | `webui/src/components/ab-torrent-list.vue` | 种子列表组件(两种页面复用) | +| Create | `webui/src/pages/index/bangumi/[id]/torrents.vue` | 番剧种子列表页 | +| Create | `webui/src/pages/index/bangumi/others/torrents.vue` | 孤儿种子列表页 | +| Modify | `webui/src/pages/index/bangumi.vue` | Others 卡片 | +| Modify | `webui/src/i18n/zh-CN.json` | 中文翻译 | +| Modify | `webui/src/i18n/en.json` | 英文翻译 | + +--- + +### Task 1: 数据库层 — 新增 4 个方法 + +**Files:** +- Modify: `backend/src/module/database/torrent.py` (在 `update_qb_hash` 方法之后,第 103 行后) + +- [ ] **Step 1: 添加 4 个方法** + +```python + def search_by_bangumi_id(self, bangumi_id: int) -> list[Torrent]: + result = self.session.execute( + select(Torrent).where(Torrent.bangumi_id == bangumi_id) + ) + return list(result.scalars().all()) + + def search_orphans(self) -> list[Torrent]: + result = self.session.execute( + select(Torrent).where(Torrent.bangumi_id == None) # noqa: E711 + ) + return list(result.scalars().all()) + + def delete_one(self, torrent_id: int) -> bool: + torrent = self.search(torrent_id) + if torrent is None: + return False + self.session.delete(torrent) + self.session.commit() + logger.debug("Deleted torrent %s.", torrent_id) + return True + + def delete_orphans(self) -> int: + result = self.session.execute( + select(Torrent).where(Torrent.bangumi_id == None) # noqa: E711 + ) + torrents = list(result.scalars().all()) + count = len(torrents) + for t in torrents: + self.session.delete(t) + if count > 0: + self.session.commit() + logger.debug("Deleted %s orphan torrents.", count) + return count +``` + +- [ ] **Step 2: 验证语法** + +Run: `python3 -c "import py_compile; py_compile.compile('backend/src/module/database/torrent.py', doraise=True); print('OK')"` + +- [ ] **Step 3: Commit** + +```bash +git add backend/src/module/database/torrent.py +git commit -m "feat(database): add torrent query and delete methods for management" +``` + +--- + +### Task 2: API 层 — 新增 6 个端点 + +**Files:** +- Modify: `backend/src/module/api/bangumi.py` + +- [ ] **Step 1: 修改 import** + +将第 10 行: +```python +from module.models import APIResponse, Bangumi, BangumiUpdate +``` +改为: +```python +from module.models import APIResponse, Bangumi, BangumiUpdate, ResponseModel, Torrent +``` + +- [ ] **Step 2: 在文件末尾添加端点** + +**路由注册顺序关键**:orphans 端点必须在 `{id}/torrents` 之前,否则 FastAPI 把字符串 `"torrents"` 当作 `{id}` 参数。 + +```python +# ── Torrent Management ── +# orphans 端点必须在 {id}/torrents 之前注册,避免路由冲突 + + +@router.get( + "/torrents/orphans", + response_model=list[Torrent], + dependencies=[Depends(get_current_user)], +) +async def get_orphan_torrents(): + with TorrentManager() as manager: + return manager.torrent.search_orphans() + + +@router.delete( + "/torrents/orphans", + response_model=APIResponse, + dependencies=[Depends(get_current_user)], +) +async def delete_orphan_torrents(): + with TorrentManager() as manager: + count = manager.torrent.delete_orphans() + return u_response( + ResponseModel( + status=True, + status_code=200, + msg_en=f"Deleted {count} orphan torrents.", + msg_zh=f"已删除 {count} 条未匹配种子。", + ) + ) + + +@router.delete( + "/torrents/orphans/{torrent_id}", + response_model=APIResponse, + dependencies=[Depends(get_current_user)], +) +async def delete_single_orphan_torrent(torrent_id: int): + with TorrentManager() as manager: + torrent = manager.torrent.search(torrent_id) + if torrent is None or torrent.bangumi_id is not None: + return u_response( + ResponseModel( + status=False, + status_code=404, + msg_en=f"Orphan torrent {torrent_id} not found.", + msg_zh=f"未找到孤儿种子 {torrent_id}。", + ) + ) + manager.torrent.delete_one(torrent_id) + return u_response( + ResponseModel( + status=True, + status_code=200, + msg_en=f"Deleted torrent {torrent_id}.", + msg_zh=f"已删除种子 {torrent_id}。", + ) + ) + + +@router.get( + "/{bangumi_id}/torrents", + response_model=list[Torrent], + dependencies=[Depends(get_current_user)], +) +async def get_bangumi_torrents(bangumi_id: int): + with TorrentManager() as manager: + return manager.torrent.search_by_bangumi_id(bangumi_id) + + +@router.delete( + "/{bangumi_id}/torrents", + response_model=APIResponse, + dependencies=[Depends(get_current_user)], +) +async def delete_bangumi_torrents(bangumi_id: int): + with TorrentManager() as manager: + count = manager.torrent.delete_by_bangumi_id(bangumi_id) + return u_response( + ResponseModel( + status=True, + status_code=200, + msg_en=f"Deleted {count} torrents for bangumi {bangumi_id}.", + msg_zh=f"已删除番剧 {bangumi_id} 的 {count} 条种子。", + ) + ) + + +@router.delete( + "/{bangumi_id}/torrents/{torrent_id}", + response_model=APIResponse, + dependencies=[Depends(get_current_user)], +) +async def delete_single_torrent(bangumi_id: int, torrent_id: int): + with TorrentManager() as manager: + torrent = manager.torrent.search(torrent_id) + if torrent is None or torrent.bangumi_id != bangumi_id: + return u_response( + ResponseModel( + status=False, + status_code=404, + msg_en=f"Torrent {torrent_id} not found under bangumi {bangumi_id}.", + msg_zh=f"番剧 {bangumi_id} 下未找到种子 {torrent_id}。", + ) + ) + manager.torrent.delete_one(torrent_id) + return u_response( + ResponseModel( + status=True, + status_code=200, + msg_en=f"Deleted torrent {torrent_id}.", + msg_zh=f"已删除种子 {torrent_id}。", + ) + ) +``` + +- [ ] **Step 3: 验证语法** + +Run: `python3 -c "import py_compile; py_compile.compile('backend/src/module/api/bangumi.py', doraise=True); print('OK')"` + +- [ ] **Step 4: Commit** + +```bash +git add backend/src/module/api/bangumi.py +git commit -m "feat(api): add torrent management endpoints for bangumi" +``` + +--- + +### Task 3: 前端类型和 API 函数 + +**Files:** +- Modify: `webui/types/torrent.ts` +- Modify: `webui/src/api/bangumi.ts` + +- [ ] **Step 1: 扩展 Torrent 类型** + +替换 `webui/types/torrent.ts` 全部内容: + +```typescript +export interface Torrent { + id: number; + name: string; + url: string; + homepage: string | null; + downloaded: boolean; + bangumi_id: number | null; + rss_id: number | null; + qb_hash: string | null; +} +``` + +- [ ] **Step 2: 在 bangumi.ts 顶部添加 Torrent 类型导入** + +在 `import type { ApiSuccess } from '#/api';` 后添加: + +```typescript +import type { Torrent } from '#/torrent'; +``` + +- [ ] **Step 3: 在 apiBangumi 对象末尾添加 6 个方法** + +在 `getNeedsReview` 方法后添加: + +```typescript + // ── Torrent Management ── + + async getTorrents(bangumiId: number) { + const { data } = await axios.get( + `api/v1/bangumi/${bangumiId}/torrents` + ); + return data; + }, + + async deleteAllTorrents(bangumiId: number) { + const { data } = await axios.delete( + `api/v1/bangumi/${bangumiId}/torrents` + ); + return data; + }, + + async deleteTorrent(bangumiId: number, torrentId: number) { + const { data } = await axios.delete( + `api/v1/bangumi/${bangumiId}/torrents/${torrentId}` + ); + return data; + }, + + async getOrphanTorrents() { + const { data } = await axios.get( + 'api/v1/bangumi/torrents/orphans' + ); + return data; + }, + + async deleteOrphanTorrents() { + const { data } = await axios.delete( + 'api/v1/bangumi/torrents/orphans' + ); + return data; + }, + + async deleteOrphanTorrent(torrentId: number) { + const { data } = await axios.delete( + `api/v1/bangumi/torrents/orphans/${torrentId}` + ); + return data; + }, +``` + +- [ ] **Step 4: 验证前端编译** + +Run: `cd webui && pnpm test:build` + +- [ ] **Step 5: Commit** + +```bash +git add webui/types/torrent.ts webui/src/api/bangumi.ts +git commit -m "feat(webui): add torrent management API functions and Torrent type" +``` + +--- + +### Task 4: 种子列表组件 + +**Files:** +- Create: `webui/src/components/ab-torrent-list.vue` + +- [ ] **Step 1: 创建组件** + +项目使用 `useApi` hook(`webui/src/hooks/useApi.ts`),接口为 `{ execute, isLoading }`。`execute` 直接传参数。`showMessage: true` 时自动显示后端返回的 `msg_zh/msg_en`。 + +```vue + + + + + +``` + +> **实现注意**:CSS 变量名(如 `--bg-secondary`、`--text-secondary`)需要参考项目实际使用的变量。如果 UnoCSS 是主要样式方案,可能需要改用 UnoCSS class。实现时读取现有组件的样式来匹配。 + +- [ ] **Step 2: Commit** + +```bash +git add webui/src/components/ab-torrent-list.vue +git commit -m "feat(webui): add torrent list component" +``` + +--- + +### Task 5: 种子列表页面 + +**Files:** +- Create: `webui/src/pages/index/bangumi/[id]/torrents.vue` +- Create: `webui/src/pages/index/bangumi/others/torrents.vue` + +项目使用 `unplugin-vue-router` file-based routing。创建页面文件即自动注册路由。 + +- [ ] **Step 1: 创建番剧种子列表页** + +`webui/src/pages/index/bangumi/[id]/torrents.vue`: + +```vue + + + +``` + +- [ ] **Step 2: 创建孤儿种子列表页** + +`webui/src/pages/index/bangumi/others/torrents.vue`: + +```vue + + + +``` + +- [ ] **Step 3: 验证编译** + +Run: `cd webui && pnpm test:build` + +- [ ] **Step 4: Commit** + +```bash +git add webui/src/pages/index/bangumi/ +git commit -m "feat(webui): add torrent list pages for bangumi and orphans" +``` + +--- + +### Task 6: Others 卡片 + +**Files:** +- Modify: `webui/src/pages/index/bangumi.vue` + +> **注意**:此 task 需要读取 `bangumi.vue` 和 `ab-bangumi-card.vue` 的完整代码来确定插入位置和方式。以下为指导性描述。 + +- [ ] **Step 1: 读取 bangumi.vue 和 ab-bangumi-card.vue** + +了解番剧列表的渲染逻辑和卡片组件的 props 接口。 + +- [ ] **Step 2: 添加孤儿数量状态** + +在 bangumi.vue 的 ` + + + + From 8866f1bbfef01487a63764b70e018f74149f59d6 Mon Sep 17 00:00:00 2001 From: ZZzzswszzZZ Date: Mon, 6 Apr 2026 14:33:36 +0800 Subject: [PATCH 12/20] feat(i18n): add torrent management translation keys Co-Authored-By: Claude Opus 4.6 --- webui/src/i18n/en.json | 13 +++++++++++++ webui/src/i18n/zh-CN.json | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/webui/src/i18n/en.json b/webui/src/i18n/en.json index dbaa4ef1d..4eecccd26 100644 --- a/webui/src/i18n/en.json +++ b/webui/src/i18n/en.json @@ -154,6 +154,9 @@ "step3_desc": "AutoBangumi will automatically download and rename new episodes for you.", "add_rss_btn": "Add RSS Feed" }, + "others": { + "title": "Others" + }, "rule": { "apply": "Apply", "delete": "Delete", @@ -181,6 +184,16 @@ "season": "Season", "year": "Year", "yes_btn": "Yes" + }, + "torrents": { + "title": "Torrents", + "delete": "Delete", + "deleteAll": "Delete All", + "downloaded": "Downloaded", + "empty": "No torrents", + "source": { + "manual": "Manual" + } } }, "log": { diff --git a/webui/src/i18n/zh-CN.json b/webui/src/i18n/zh-CN.json index 5081dc19f..77f3e155a 100644 --- a/webui/src/i18n/zh-CN.json +++ b/webui/src/i18n/zh-CN.json @@ -154,6 +154,9 @@ "step3_desc": "AutoBangumi 将自动下载并重命名新剧集。", "add_rss_btn": "添加 RSS 订阅" }, + "others": { + "title": "未匹配种子" + }, "rule": { "apply": "应用", "delete": "删除", @@ -181,6 +184,16 @@ "season": "季度", "year": "年份", "yes_btn": "是" + }, + "torrents": { + "title": "种子列表", + "delete": "删除", + "deleteAll": "清空所有", + "downloaded": "已下载", + "empty": "暂无种子", + "source": { + "manual": "手动/订阅" + } } }, "log": { From bac82c6556a752bb3f7fbcb7b9f4bd9a09eccfe3 Mon Sep 17 00:00:00 2001 From: ZZzzswszzZZ Date: Mon, 6 Apr 2026 14:38:54 +0800 Subject: [PATCH 13/20] feat(webui): add torrent list pages for bangumi and orphans Co-Authored-By: Claude Opus 4.6 --- .../src/pages/index/bangumi/[id]/torrents.vue | 35 +++++++++++++++++++ .../pages/index/bangumi/others/torrents.vue | 33 +++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 webui/src/pages/index/bangumi/[id]/torrents.vue create mode 100644 webui/src/pages/index/bangumi/others/torrents.vue diff --git a/webui/src/pages/index/bangumi/[id]/torrents.vue b/webui/src/pages/index/bangumi/[id]/torrents.vue new file mode 100644 index 000000000..d7b0a51af --- /dev/null +++ b/webui/src/pages/index/bangumi/[id]/torrents.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/webui/src/pages/index/bangumi/others/torrents.vue b/webui/src/pages/index/bangumi/others/torrents.vue new file mode 100644 index 000000000..dddfb6a70 --- /dev/null +++ b/webui/src/pages/index/bangumi/others/torrents.vue @@ -0,0 +1,33 @@ + + + + + From d380d6bb4a538d712c28246d16c5cf3d7854d642 Mon Sep 17 00:00:00 2001 From: ZZzzswszzZZ Date: Mon, 6 Apr 2026 14:43:52 +0800 Subject: [PATCH 14/20] feat(webui): add Others card to bangumi list for orphan torrents Co-Authored-By: Claude Opus 4.6 --- webui/src/pages/index/bangumi.vue | 118 +++++++++++++++++++++++++++++- 1 file changed, 117 insertions(+), 1 deletion(-) diff --git a/webui/src/pages/index/bangumi.vue b/webui/src/pages/index/bangumi.vue index d454afa67..7f9264ffe 100644 --- a/webui/src/pages/index/bangumi.vue +++ b/webui/src/pages/index/bangumi.vue @@ -15,10 +15,28 @@ const skeletonCount = 8; // Number of skeleton cards to show const refreshing = ref(false); +// Orphan torrents count for Others card +const orphanCount = ref(0); + +async function loadOrphanCount() { + try { + const orphans = await apiBangumi.getOrphanTorrents(); + orphanCount.value = orphans.length; + } catch { + orphanCount.value = 0; + } +} + +const router = useRouter(); + +function goToOrphans() { + router.push('/bangumi/others/torrents'); +} + async function onRefresh() { refreshing.value = true; try { - await getAll(); + await Promise.all([getAll(), loadOrphanCount()]); } finally { refreshing.value = false; } @@ -26,6 +44,7 @@ async function onRefresh() { onActivated(() => { getAll(); + loadOrphanCount(); }); // Group bangumi by official_title + season @@ -175,6 +194,25 @@ function groupNeedsReview(group: BangumiGroup): boolean { + + +
+
+ ? +
{{ orphanCount }}
+
+
+
{{ $t('homepage.others.title') }}
+
+
@@ -340,6 +378,84 @@ function groupNeedsReview(group: BangumiGroup): boolean { } } +// Others card for orphan torrents +.others-card { + width: 150px; + cursor: pointer; + user-select: none; + + &:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 4px; + border-radius: var(--radius-md); + } +} + +.others-poster { + position: relative; + aspect-ratio: 5 / 7; + border-radius: var(--radius-md); + overflow: hidden; + box-shadow: var(--shadow-md); + display: flex; + align-items: center; + justify-content: center; + background: var(--color-surface-hover); + border: 2px dashed var(--color-border); + transition: box-shadow var(--transition-fast), transform var(--transition-fast); + + .others-card:hover &, + .others-card:focus-visible & { + box-shadow: var(--shadow-lg); + transform: translateY(-2px); + } + + @include forTouch { + .others-card:hover & { + transform: none; + } + } +} + +.others-icon { + font-size: 48px; + font-weight: 700; + color: var(--color-text-muted); +} + +.others-count-badge { + position: absolute; + top: -8px; + right: -8px; + min-width: 20px; + height: 20px; + padding: 0 6px; + border-radius: 10px; + background: var(--color-primary); + color: #fff; + font-size: 12px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; + box-shadow: 0 2px 6px rgba(124, 77, 255, 0.4); +} + +.others-info { + padding: 8px 2px 4px; +} + +.others-title { + font-size: 14px; + font-weight: 500; + line-height: 1.4; + color: var(--color-text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .bangumi-group-wrapper { position: relative; overflow: visible; From ba96634c43b1cd0ed65ecf139e5a3ba481e081f6 Mon Sep 17 00:00:00 2001 From: ZZzzswszzZZ Date: Mon, 6 Apr 2026 15:52:36 +0800 Subject: [PATCH 15/20] fix: address code review issues for torrent management - Remove redundant inline ResponseModel imports (already in top-level) - Use onActivated instead of onMounted in torrent pages (KeepAlive compat) - Add confirm dialog before delete-all action Co-Authored-By: Claude Opus 4.6 --- backend/src/module/api/bangumi.py | 36 ++++++++----------- webui/src/components/ab-torrent-list.vue | 7 +++- .../src/pages/index/bangumi/[id]/torrents.vue | 2 +- .../pages/index/bangumi/others/torrents.vue | 2 +- 4 files changed, 23 insertions(+), 24 deletions(-) diff --git a/backend/src/module/api/bangumi.py b/backend/src/module/api/bangumi.py index 1e680e3cf..e2a50db43 100644 --- a/backend/src/module/api/bangumi.py +++ b/backend/src/module/api/bangumi.py @@ -7,7 +7,7 @@ from module.conf import settings from module.database import Database from module.manager import TorrentManager -from module.models import APIResponse, Bangumi, BangumiUpdate, Torrent +from module.models import APIResponse, Bangumi, BangumiUpdate, ResponseModel, Torrent from module.parser.analyser.offset_detector import ( OffsetSuggestion as DetectorSuggestion, ) @@ -404,7 +404,6 @@ async def get_orphan_torrents(): async def delete_orphan_torrents(): with TorrentManager() as manager: count = manager.torrent.delete_orphans() - from module.models import ResponseModel return u_response( ResponseModel( status=True, @@ -424,17 +423,15 @@ async def delete_single_orphan_torrent(torrent_id: int): with TorrentManager() as manager: torrent = manager.torrent.search(torrent_id) if torrent is None or torrent.bangumi_id is not None: - from module.models import ResponseModel - return u_response( - ResponseModel( - status=False, - status_code=404, - msg_en=f"Orphan torrent {torrent_id} not found.", - msg_zh=f"未找到孤儿种子 {torrent_id}。", - ) + return JSONResponse( + status_code=404, + content={ + "status": False, + "msg_en": f"Orphan torrent {torrent_id} not found.", + "msg_zh": f"未找到孤儿种子 {torrent_id}。", + }, ) manager.torrent.delete_one(torrent_id) - from module.models import ResponseModel return u_response( ResponseModel( status=True, @@ -463,7 +460,6 @@ async def get_bangumi_torrents(bangumi_id: int): async def delete_bangumi_torrents(bangumi_id: int): with TorrentManager() as manager: count = manager.torrent.delete_by_bangumi_id(bangumi_id) - from module.models import ResponseModel return u_response( ResponseModel( status=True, @@ -483,17 +479,15 @@ async def delete_single_torrent(bangumi_id: int, torrent_id: int): with TorrentManager() as manager: torrent = manager.torrent.search(torrent_id) if torrent is None or torrent.bangumi_id != bangumi_id: - from module.models import ResponseModel - return u_response( - ResponseModel( - status=False, - status_code=404, - msg_en=f"Torrent {torrent_id} not found under bangumi {bangumi_id}.", - msg_zh=f"番剧 {bangumi_id} 下未找到种子 {torrent_id}。", - ) + return JSONResponse( + status_code=404, + content={ + "status": False, + "msg_en": f"Torrent {torrent_id} not found under bangumi {bangumi_id}.", + "msg_zh": f"番剧 {bangumi_id} 下未找到种子 {torrent_id}。", + }, ) manager.torrent.delete_one(torrent_id) - from module.models import ResponseModel return u_response( ResponseModel( status=True, diff --git a/webui/src/components/ab-torrent-list.vue b/webui/src/components/ab-torrent-list.vue index 9b6874ced..d70a7fda2 100644 --- a/webui/src/components/ab-torrent-list.vue +++ b/webui/src/components/ab-torrent-list.vue @@ -27,6 +27,11 @@ const { execute: execDeleteOne, isLoading: deletingOne } = useApi( } ); +async function handleDeleteAll() { + if (!confirm('确认清空所有种子?')) return; + await execDeleteAll(); +} + const { execute: execDeleteAll, isLoading: deletingAll } = useApi( async () => { if (props.isOrphan) { @@ -75,7 +80,7 @@ const { execute: execDeleteAll, isLoading: deletingAll } = useApi(