mirror of
https://github.com/EstrellaXD/Auto_Bangumi.git
synced 2026-11-04 06:38:05 +08:00
fix(manager): remove orphaned per-sub RSS feeds when deleting a bangumi (#1053)
Deleting a bangumi left its individually-subscribed (aggregate=False) RSSItem enabled, so the WebUI kept showing the feed as active and the RSS loop kept polling it. delete_rule now removes per-sub feeds that no remaining bangumi references, splitting comma-joined rss_link values and matching URLs exactly. Aggregate feeds and feeds still referenced by any bangumi (including soft-disabled ones) are never touched. Adds RSSDatabase.search_url for exact-URL lookup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JhTduCYBGeHETU7dSWoh37
This commit is contained in:
@@ -93,6 +93,11 @@ class RSSDatabase:
|
||||
async def search_id(self, _id: int) -> RSSItem | None:
|
||||
return await self.session.get(RSSItem, _id)
|
||||
|
||||
async def search_url(self, url: str) -> RSSItem | None:
|
||||
statement = select(RSSItem).where(RSSItem.url == url)
|
||||
result = await self.session.execute(statement)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def search_all(self) -> list[RSSItem]:
|
||||
result = await self.session.execute(select(RSSItem))
|
||||
return list(result.scalars().all())
|
||||
|
||||
@@ -52,18 +52,35 @@ class TorrentManager:
|
||||
msg_zh=f"无法找到 {data.official_title} 的种子",
|
||||
)
|
||||
|
||||
async def _delete_orphan_sub_rss(self, data: Bangumi) -> None:
|
||||
"""删除该番剧独立订阅(aggregate=False)且不再被引用的 RSS 条目(#1053)。
|
||||
|
||||
聚合订阅由多个番剧共享,永不在此处删除;rss_link 可能是逗号拼接的
|
||||
多个链接(见 BangumiDatabase.match_list),逐个拆分精确匹配。
|
||||
"""
|
||||
urls = {u.strip() for u in (data.rss_link or "").split(",") if u.strip()}
|
||||
if not urls:
|
||||
return
|
||||
# 含软删除(deleted=True)的番剧:其订阅需保留以便重新启用
|
||||
still_referenced: set[str] = set()
|
||||
for bangumi in await self.db.bangumi.search_all():
|
||||
still_referenced.update(
|
||||
u.strip() for u in (bangumi.rss_link or "").split(",") if u.strip()
|
||||
)
|
||||
for url in urls - still_referenced:
|
||||
rss_item = await self.db.rss.search_url(url)
|
||||
if rss_item and not rss_item.aggregate:
|
||||
await self.db.rss.delete(rss_item.id)
|
||||
logger.info(f"[Manager] Delete orphan RSS feed {url}")
|
||||
|
||||
async def delete_rule(self, _id: int | str, file: bool = False):
|
||||
data = await self.db.bangumi.search_id(int(_id))
|
||||
if isinstance(data, Bangumi):
|
||||
# NOTE: RSSDatabase.delete() takes an int RSS-item id, not a
|
||||
# title; there is no reliable bangumi -> RSSItem link to look one
|
||||
# up here (RSSItem.url may be shared by several aggregated
|
||||
# bangumi), so we deliberately don't touch RSS items on rule
|
||||
# deletion rather than risk deleting a feed other bangumi still
|
||||
# depend on.
|
||||
# Clean up torrent records so re-adding the same anime can re-download
|
||||
await self.db.torrent.delete_by_bangumi_id(int(_id))
|
||||
await self.db.bangumi.delete_one(int(_id))
|
||||
# 番剧删除后清理其独立订阅的孤儿 RSS;聚合订阅不受影响
|
||||
await self._delete_orphan_sub_rss(data)
|
||||
torrent_message = None
|
||||
if file:
|
||||
# Only the file-cleanup path needs the downloader, so an
|
||||
|
||||
@@ -1021,6 +1021,23 @@ class TestRSSDeleteWithTorrents:
|
||||
assert result is True
|
||||
|
||||
|
||||
class TestRSSSearchUrl:
|
||||
"""Tests for RSSDatabase.search_url (exact-URL lookup, #1053)."""
|
||||
|
||||
async def test_search_url_returns_matching_item(self, db_session):
|
||||
rss_db = RSSDatabase(db_session)
|
||||
rss = RSSItem(url="https://mikanani.me/RSS/Bangumi?bangumiId=9", name="Sub")
|
||||
await rss_db.add(rss)
|
||||
|
||||
found = await rss_db.search_url("https://mikanani.me/RSS/Bangumi?bangumiId=9")
|
||||
assert found is not None
|
||||
assert found.id == rss.id
|
||||
|
||||
async def test_search_url_missing_returns_none(self, db_session):
|
||||
rss_db = RSSDatabase(db_session)
|
||||
assert await rss_db.search_url("https://example.com/absent") is None
|
||||
|
||||
|
||||
class TestOrphanTorrents:
|
||||
"""Tests for TorrentDatabase orphan-torrent operations (bangumi_id IS NULL)."""
|
||||
|
||||
|
||||
89
backend/src/test/test_manager_torrent.py
Normal file
89
backend/src/test/test_manager_torrent.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""Tests for TorrentManager.delete_rule RSS cleanup (#1053).
|
||||
|
||||
删除番剧时应一并清理其独立订阅(aggregate=False)的孤儿 RSS 条目;
|
||||
聚合订阅与仍被其他番剧引用的订阅不受影响。
|
||||
"""
|
||||
|
||||
from module.database import Database
|
||||
from module.manager import TorrentManager
|
||||
from test.factories import make_bangumi, make_rss_item
|
||||
|
||||
SUB_URL = "https://mikanani.me/RSS/Bangumi?bangumiId=9&subgroupid=1"
|
||||
|
||||
|
||||
async def _seed(db: Database, *, bangumi=(), rss=()):
|
||||
for b in bangumi:
|
||||
await db.bangumi.add(b)
|
||||
for r in rss:
|
||||
await db.rss.add(r)
|
||||
|
||||
|
||||
class TestDeleteRuleRSSCleanup:
|
||||
async def test_delete_rule_removes_orphan_sub_rss(self, db_engine):
|
||||
async with Database(engine=db_engine) as db:
|
||||
await _seed(
|
||||
db,
|
||||
bangumi=[make_bangumi(id=1, rss_link=SUB_URL)],
|
||||
rss=[make_rss_item(url=SUB_URL, aggregate=False)],
|
||||
)
|
||||
manager = TorrentManager(db)
|
||||
|
||||
resp = await manager.delete_rule(1, file=False)
|
||||
|
||||
assert resp.status is True
|
||||
assert await db.rss.search_url(SUB_URL) is None
|
||||
|
||||
async def test_delete_rule_keeps_aggregate_rss(self, db_engine):
|
||||
async with Database(engine=db_engine) as db:
|
||||
await _seed(
|
||||
db,
|
||||
bangumi=[make_bangumi(id=1, rss_link=SUB_URL)],
|
||||
rss=[make_rss_item(url=SUB_URL, aggregate=True)],
|
||||
)
|
||||
manager = TorrentManager(db)
|
||||
|
||||
await manager.delete_rule(1, file=False)
|
||||
|
||||
assert await db.rss.search_url(SUB_URL) is not None
|
||||
|
||||
async def test_delete_rule_keeps_rss_referenced_by_other_bangumi(self, db_engine):
|
||||
async with Database(engine=db_engine) as db:
|
||||
await _seed(
|
||||
db,
|
||||
bangumi=[
|
||||
make_bangumi(id=1, rss_link=SUB_URL),
|
||||
make_bangumi(
|
||||
id=2,
|
||||
official_title="Other Anime",
|
||||
title_raw="Other Anime Raw",
|
||||
rss_link=f"https://example.com/other,{SUB_URL}",
|
||||
),
|
||||
],
|
||||
rss=[make_rss_item(url=SUB_URL, aggregate=False)],
|
||||
)
|
||||
manager = TorrentManager(db)
|
||||
|
||||
await manager.delete_rule(1, file=False)
|
||||
|
||||
assert await db.rss.search_url(SUB_URL) is not None
|
||||
|
||||
async def test_delete_rule_comma_joined_link_removes_each_orphan_feed(
|
||||
self, db_engine
|
||||
):
|
||||
url_a = "https://mikanani.me/RSS/Bangumi?bangumiId=1"
|
||||
url_b = "https://mikanani.me/RSS/Bangumi?bangumiId=2"
|
||||
async with Database(engine=db_engine) as db:
|
||||
await _seed(
|
||||
db,
|
||||
bangumi=[make_bangumi(id=1, rss_link=f"{url_a},{url_b}")],
|
||||
rss=[
|
||||
make_rss_item(url=url_a, aggregate=False),
|
||||
make_rss_item(url=url_b, aggregate=False),
|
||||
],
|
||||
)
|
||||
manager = TorrentManager(db)
|
||||
|
||||
await manager.delete_rule(1, file=False)
|
||||
|
||||
assert await db.rss.search_url(url_a) is None
|
||||
assert await db.rss.search_url(url_b) is None
|
||||
Reference in New Issue
Block a user