From 6cbe7090fee29f5bd596bcc48b1b959a0e7d9258 Mon Sep 17 00:00:00 2001 From: Estrella Pan Date: Mon, 23 Feb 2026 09:45:58 +0100 Subject: [PATCH] fix(database): clean up torrent records on bangumi deletion When a bangumi was deleted, its associated Torrent records remained in the database. This prevented re-downloading the same torrents if the user re-added the anime, because check_new() deduplicates by URL and would filter out the orphaned records. Now delete_rule() removes Torrent records before deleting the Bangumi, so re-adding the same anime correctly treats those torrents as new. Co-Authored-By: Claude Opus 4.6 --- backend/src/module/database/torrent.py | 16 +++++++ backend/src/module/manager/torrent.py | 2 + backend/src/test/test_database.py | 60 ++++++++++++++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/backend/src/module/database/torrent.py b/backend/src/module/database/torrent.py index 341ac5c2..d4fb5430 100644 --- a/backend/src/module/database/torrent.py +++ b/backend/src/module/database/torrent.py @@ -70,6 +70,22 @@ class TorrentDatabase: ) return list(result.scalars().all()) + def delete_by_bangumi_id(self, bangumi_id: int) -> int: + """Delete all torrent records associated with a bangumi. + + Returns the number of deleted records. + """ + statement = select(Torrent).where(Torrent.bangumi_id == bangumi_id) + result = self.session.execute(statement) + 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 torrent records for bangumi_id %s.", count, bangumi_id) + return count + def search_by_url(self, url: str) -> Torrent | None: """Find torrent by URL.""" result = self.session.execute(select(Torrent).where(Torrent.url == url)) diff --git a/backend/src/module/manager/torrent.py b/backend/src/module/manager/torrent.py index 354d4002..79ac9526 100644 --- a/backend/src/module/manager/torrent.py +++ b/backend/src/module/manager/torrent.py @@ -46,6 +46,8 @@ class TorrentManager(Database): if isinstance(data, Bangumi): async with DownloadClient() as client: self.rss.delete(data.official_title) + # Clean up torrent records so re-adding the same anime can re-download + self.torrent.delete_by_bangumi_id(int(_id)) self.bangumi.delete_one(int(_id)) torrent_message = None if file: diff --git a/backend/src/test/test_database.py b/backend/src/test/test_database.py index 363c2971..971dbbf3 100644 --- a/backend/src/test/test_database.py +++ b/backend/src/test/test_database.py @@ -440,6 +440,66 @@ def test_add_with_semantic_duplicate_creates_alias(db_session): assert "Frieren Beyond Journey's End" in aliases +class TestDeleteByBangumiId: + """Tests for TorrentDatabase.delete_by_bangumi_id.""" + + def test_deletes_matching_torrents(self, db_session): + db = TorrentDatabase(db_session) + for i in range(3): + db.add(Torrent(name=f"torrent_{i}", url=f"https://example.com/{i}", bangumi_id=10)) + assert len(db.search_all()) == 3 + + count = db.delete_by_bangumi_id(10) + assert count == 3 + assert len(db.search_all()) == 0 + + def test_leaves_other_bangumi_torrents(self, db_session): + db = TorrentDatabase(db_session) + db.add(Torrent(name="keep", url="https://example.com/keep", bangumi_id=20)) + db.add(Torrent(name="delete", url="https://example.com/delete", bangumi_id=30)) + + count = db.delete_by_bangumi_id(30) + assert count == 1 + remaining = db.search_all() + assert len(remaining) == 1 + assert remaining[0].name == "keep" + + def test_no_match_returns_zero(self, db_session): + db = TorrentDatabase(db_session) + db.add(Torrent(name="unrelated", url="https://example.com/1", bangumi_id=5)) + + count = db.delete_by_bangumi_id(999) + assert count == 0 + assert len(db.search_all()) == 1 + + def test_skips_null_bangumi_id(self, db_session): + db = TorrentDatabase(db_session) + db.add(Torrent(name="orphan", url="https://example.com/orphan", bangumi_id=None)) + db.add(Torrent(name="target", url="https://example.com/target", bangumi_id=7)) + + count = db.delete_by_bangumi_id(7) + assert count == 1 + remaining = db.search_all() + assert len(remaining) == 1 + assert remaining[0].bangumi_id is None + + def test_check_new_finds_urls_after_cleanup(self, db_session): + """Core scenario: after deleting torrent records, check_new should treat those URLs as new.""" + db = TorrentDatabase(db_session) + db.add(Torrent(name="ep01", url="https://mikan.me/t/001", bangumi_id=42)) + db.add(Torrent(name="ep02", url="https://mikan.me/t/002", bangumi_id=42)) + + # Before cleanup: check_new filters them out + incoming = [Torrent(name="ep01", url="https://mikan.me/t/001")] + assert db.check_new(incoming) == [] + + # After cleanup: same URLs are now "new" + db.delete_by_bangumi_id(42) + new = db.check_new(incoming) + assert len(new) == 1 + assert new[0].url == "https://mikan.me/t/001" + + def test_groups_are_similar(): """Test group name similarity detection.""" from module.database.bangumi import _groups_are_similar