test(renamer): add comprehensive tests for offset lookup functionality

Add tests covering:
- _parse_bangumi_id_from_tags: tag parsing with various formats
- gen_path with offsets: episode/season offset application
- _lookup_offsets: multi-tier lookup (qb_hash, tags, name, path)
- TorrentDatabase hash lookup methods (search_by_qb_hash, search_by_url, update_qb_hash)

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
Estrella Pan
2026-01-26 08:24:23 +01:00
parent 34332d27af
commit bfb94145cb
2 changed files with 553 additions and 0 deletions

View File

@@ -94,3 +94,134 @@ def test_rss_database(db_session):
db.add(RSSItem(url=rss_url, name="Test RSS"))
result = db.search_id(1)
assert result.url == rss_url
# ---------------------------------------------------------------------------
# TorrentDatabase qb_hash methods
# ---------------------------------------------------------------------------
def test_torrent_search_by_qb_hash(db_session):
"""Test searching torrent by qBittorrent hash."""
db = TorrentDatabase(db_session)
# Create torrent with qb_hash
torrent = Torrent(
name="[SubGroup] Test Anime - 01 [1080p].mkv",
url="https://example.com/torrent1",
qb_hash="abc123def456",
)
db.add(torrent)
# Search by qb_hash
result = db.search_by_qb_hash("abc123def456")
assert result is not None
assert result.name == torrent.name
assert result.qb_hash == "abc123def456"
def test_torrent_search_by_qb_hash_not_found(db_session):
"""Test searching non-existent qb_hash returns None."""
db = TorrentDatabase(db_session)
result = db.search_by_qb_hash("nonexistent_hash")
assert result is None
def test_torrent_search_by_url(db_session):
"""Test searching torrent by URL."""
db = TorrentDatabase(db_session)
url = "https://mikanani.me/Download/torrent123.torrent"
torrent = Torrent(
name="[SubGroup] Test Anime - 02 [1080p].mkv",
url=url,
)
db.add(torrent)
# Search by URL
result = db.search_by_url(url)
assert result is not None
assert result.url == url
assert result.name == torrent.name
def test_torrent_search_by_url_not_found(db_session):
"""Test searching non-existent URL returns None."""
db = TorrentDatabase(db_session)
result = db.search_by_url("https://nonexistent.com/torrent.torrent")
assert result is None
def test_torrent_update_qb_hash(db_session):
"""Test updating qb_hash for existing torrent."""
db = TorrentDatabase(db_session)
# Create torrent without qb_hash
torrent = Torrent(
name="[SubGroup] Test Anime - 03 [1080p].mkv",
url="https://example.com/torrent3",
)
db.add(torrent)
assert torrent.qb_hash is None
# Update qb_hash
success = db.update_qb_hash(torrent.id, "new_hash_value")
assert success is True
# Verify update
result = db.search(torrent.id)
assert result.qb_hash == "new_hash_value"
def test_torrent_update_qb_hash_nonexistent(db_session):
"""Test updating qb_hash for non-existent torrent returns False."""
db = TorrentDatabase(db_session)
success = db.update_qb_hash(99999, "some_hash")
assert success is False
def test_torrent_with_bangumi_id(db_session):
"""Test torrent with bangumi_id for offset lookup."""
db = TorrentDatabase(db_session)
# Create torrent linked to a bangumi
torrent = Torrent(
name="[SubGroup] Test Anime - 04 [1080p].mkv",
url="https://example.com/torrent4",
bangumi_id=42,
qb_hash="hash_for_bangumi_42",
)
db.add(torrent)
# Search and verify bangumi_id is preserved
result = db.search_by_qb_hash("hash_for_bangumi_42")
assert result is not None
assert result.bangumi_id == 42
def test_torrent_qb_hash_index_efficient(db_session):
"""Test that qb_hash lookups work correctly with multiple torrents."""
db = TorrentDatabase(db_session)
# Add multiple torrents
torrents = [
Torrent(name=f"Torrent {i}", url=f"https://example.com/{i}", qb_hash=f"hash_{i}")
for i in range(10)
]
db.add_all(torrents)
# Verify we can find specific torrents by hash
result = db.search_by_qb_hash("hash_5")
assert result is not None
assert result.name == "Torrent 5"
result = db.search_by_qb_hash("hash_9")
assert result is not None
assert result.name == "Torrent 9"
# Non-existent hash
result = db.search_by_qb_hash("hash_100")
assert result is None

View File

@@ -470,3 +470,425 @@ class TestRenameFlow:
assert result == []
renamer.client.torrents_rename_file.assert_not_called()
# ---------------------------------------------------------------------------
# _parse_bangumi_id_from_tags
# ---------------------------------------------------------------------------
class TestParseBangumiIdFromTags:
"""Tests for Renamer._parse_bangumi_id_from_tags static method."""
def test_single_ab_tag(self):
"""Parses 'ab:123' format correctly."""
result = Renamer._parse_bangumi_id_from_tags("ab:123")
assert result == 123
def test_ab_tag_with_other_tags(self):
"""Extracts ab tag from comma-separated list."""
result = Renamer._parse_bangumi_id_from_tags("anime,ab:456,downloaded")
assert result == 456
def test_ab_tag_with_spaces(self):
"""Handles whitespace around tags."""
result = Renamer._parse_bangumi_id_from_tags(" ab:789 , other_tag ")
assert result == 789
def test_empty_string(self):
"""Returns None for empty string."""
result = Renamer._parse_bangumi_id_from_tags("")
assert result is None
def test_none_input(self):
"""Returns None for None input."""
result = Renamer._parse_bangumi_id_from_tags(None)
assert result is None
def test_no_ab_tag(self):
"""Returns None when no ab: tag present."""
result = Renamer._parse_bangumi_id_from_tags("anime,downloaded,HD")
assert result is None
def test_invalid_ab_tag_non_numeric(self):
"""Returns None when ab: tag has non-numeric value."""
result = Renamer._parse_bangumi_id_from_tags("ab:not_a_number")
assert result is None
def test_ab_tag_first_match(self):
"""Returns first ab: tag if multiple present."""
result = Renamer._parse_bangumi_id_from_tags("ab:111,ab:222")
assert result == 111
def test_ab_tag_zero(self):
"""Handles ab:0 correctly."""
result = Renamer._parse_bangumi_id_from_tags("ab:0")
assert result == 0
def test_ab_tag_large_number(self):
"""Handles large bangumi IDs."""
result = Renamer._parse_bangumi_id_from_tags("ab:999999")
assert result == 999999
# ---------------------------------------------------------------------------
# gen_path with offsets
# ---------------------------------------------------------------------------
class TestGenPathWithOffsets:
"""Tests for gen_path with episode_offset and season_offset parameters."""
def test_episode_offset_positive(self):
"""Episode offset adds to episode number."""
ep = EpisodeFile(
media_path="old.mkv", title="My Anime", season=1, episode=5, suffix=".mkv"
)
result = Renamer.gen_path(ep, "Bangumi", method="pn", episode_offset=12)
assert "E17" in result # 5 + 12 = 17
def test_episode_offset_negative(self):
"""Negative episode offset subtracts from episode number."""
ep = EpisodeFile(
media_path="old.mkv", title="My Anime", season=1, episode=15, suffix=".mkv"
)
result = Renamer.gen_path(ep, "Bangumi", method="pn", episode_offset=-12)
assert "E03" in result # 15 - 12 = 3
def test_episode_offset_negative_below_one_ignored(self):
"""Negative offset that would go below 1 is ignored."""
ep = EpisodeFile(
media_path="old.mkv", title="My Anime", season=1, episode=5, suffix=".mkv"
)
result = Renamer.gen_path(ep, "Bangumi", method="pn", episode_offset=-10)
assert "E05" in result # Would be -5, so offset ignored
def test_season_offset_positive(self):
"""Season offset adds to season number."""
ep = EpisodeFile(
media_path="old.mkv", title="My Anime", season=1, episode=5, suffix=".mkv"
)
result = Renamer.gen_path(ep, "Bangumi", method="pn", season_offset=1)
assert "S02" in result # 1 + 1 = 2
def test_season_offset_negative(self):
"""Negative season offset subtracts from season number."""
ep = EpisodeFile(
media_path="old.mkv", title="My Anime", season=3, episode=5, suffix=".mkv"
)
result = Renamer.gen_path(ep, "Bangumi", method="pn", season_offset=-1)
assert "S02" in result # 3 - 1 = 2
def test_season_offset_negative_below_one_ignored(self):
"""Negative season offset that would go below 1 is ignored."""
ep = EpisodeFile(
media_path="old.mkv", title="My Anime", season=1, episode=5, suffix=".mkv"
)
result = Renamer.gen_path(ep, "Bangumi", method="pn", season_offset=-5)
assert "S01" in result # Would be -4, so offset ignored
def test_both_offsets_combined(self):
"""Both episode and season offset applied together."""
ep = EpisodeFile(
media_path="old.mkv", title="My Anime", season=1, episode=13, suffix=".mkv"
)
result = Renamer.gen_path(
ep, "Bangumi", method="pn", episode_offset=-12, season_offset=1
)
assert "S02E01" in result # Season 1+1=2, Episode 13-12=1
def test_offset_with_advance_method(self):
"""Offset works with advance rename method."""
ep = EpisodeFile(
media_path="old.mkv", title="My Anime", season=1, episode=25, suffix=".mkv"
)
result = Renamer.gen_path(
ep, "Bangumi Name", method="advance", episode_offset=-12
)
assert result == "Bangumi Name S01E13.mkv"
def test_offset_with_subtitle_method(self):
"""Offset works with subtitle rename methods."""
sub = SubtitleFile(
media_path="sub.ass",
title="My Anime",
season=1,
episode=25,
language="zh",
suffix=".ass",
)
result = Renamer.gen_path(
sub, "Bangumi", method="subtitle_pn", episode_offset=-12
)
assert "E13" in result # 25 - 12 = 13
def test_offset_none_method_unchanged(self):
"""None method returns original path regardless of offset."""
ep = EpisodeFile(
media_path="original/path/file.mkv",
title="Test",
season=1,
episode=1,
suffix=".mkv",
)
result = Renamer.gen_path(ep, "Bangumi", method="none", episode_offset=100)
assert result == "original/path/file.mkv"
# ---------------------------------------------------------------------------
# _lookup_offsets
# ---------------------------------------------------------------------------
class TestLookupOffsets:
"""Tests for Renamer._lookup_offsets method with multi-tier lookup."""
@pytest.fixture
def renamer(self, mock_qb_client):
"""Create Renamer with mocked internals."""
with patch("module.downloader.download_client.settings") as mock_settings:
mock_settings.downloader.type = "qbittorrent"
mock_settings.downloader.host = "localhost:8080"
mock_settings.downloader.username = "admin"
mock_settings.downloader.password = "admin"
mock_settings.downloader.ssl = False
mock_settings.downloader.path = "/downloads/Bangumi"
mock_settings.bangumi_manage.group_tag = False
with patch(
"module.downloader.download_client.DownloadClient._DownloadClient__getClient",
return_value=mock_qb_client,
):
r = Renamer()
r.client = mock_qb_client
return r
def test_lookup_by_qb_hash(self, renamer, db_session):
"""First priority: lookup by qb_hash in Torrent table."""
from module.database.bangumi import BangumiDatabase
from module.database.torrent import TorrentDatabase
from module.models import Bangumi, Torrent
# Create bangumi with offsets
bangumi_db = BangumiDatabase(db_session)
bangumi = Bangumi(
official_title="Test Anime",
year="2024",
title_raw="test_raw",
season=1,
episode_offset=-12,
season_offset=1,
)
bangumi_db.add(bangumi)
# Create torrent linked to bangumi
torrent_db = TorrentDatabase(db_session)
torrent = Torrent(
name="Test Torrent",
url="https://example.com/torrent",
bangumi_id=bangumi.id,
qb_hash="abc123hash",
)
torrent_db.add(torrent)
with patch("module.manager.renamer.Database") as MockDatabase:
mock_db = MagicMock()
mock_db.__enter__ = MagicMock(return_value=mock_db)
mock_db.__exit__ = MagicMock(return_value=False)
mock_db.torrent = TorrentDatabase(db_session)
mock_db.bangumi = BangumiDatabase(db_session)
MockDatabase.return_value = mock_db
episode_offset, season_offset = renamer._lookup_offsets(
torrent_hash="abc123hash",
torrent_name="irrelevant",
save_path="/irrelevant/path",
tags="",
)
assert episode_offset == -12
assert season_offset == 1
def test_lookup_by_tag_when_hash_not_found(self, renamer, db_session):
"""Second priority: lookup by ab:ID tag when qb_hash not found."""
from module.database.bangumi import BangumiDatabase
from module.database.torrent import TorrentDatabase
from module.models import Bangumi
# Create bangumi with offsets
bangumi_db = BangumiDatabase(db_session)
bangumi = Bangumi(
official_title="Tagged Anime",
year="2024",
title_raw="tagged_raw",
season=1,
episode_offset=5,
season_offset=0,
)
bangumi_db.add(bangumi)
with patch("module.manager.renamer.Database") as MockDatabase:
mock_db = MagicMock()
mock_db.__enter__ = MagicMock(return_value=mock_db)
mock_db.__exit__ = MagicMock(return_value=False)
mock_db.torrent = TorrentDatabase(db_session)
mock_db.bangumi = BangumiDatabase(db_session)
MockDatabase.return_value = mock_db
episode_offset, season_offset = renamer._lookup_offsets(
torrent_hash="nonexistent_hash",
torrent_name="irrelevant",
save_path="/irrelevant/path",
tags=f"ab:{bangumi.id}",
)
assert episode_offset == 5
assert season_offset == 0
def test_lookup_by_torrent_name(self, renamer, db_session):
"""Third priority: lookup by torrent name matching title_raw."""
from module.database.bangumi import BangumiDatabase
from module.database.torrent import TorrentDatabase
from module.models import Bangumi
# Create bangumi with offsets
bangumi_db = BangumiDatabase(db_session)
bangumi = Bangumi(
official_title="Name Match Anime",
year="2024",
title_raw="[SubGroup] Name Match",
season=1,
episode_offset=-6,
season_offset=2,
)
bangumi_db.add(bangumi)
with patch("module.manager.renamer.Database") as MockDatabase:
mock_db = MagicMock()
mock_db.__enter__ = MagicMock(return_value=mock_db)
mock_db.__exit__ = MagicMock(return_value=False)
mock_db.torrent = TorrentDatabase(db_session)
mock_db.bangumi = BangumiDatabase(db_session)
MockDatabase.return_value = mock_db
episode_offset, season_offset = renamer._lookup_offsets(
torrent_hash="nonexistent_hash",
torrent_name="[SubGroup] Name Match - 01 [1080p].mkv",
save_path="/irrelevant/path",
tags="",
)
assert episode_offset == -6
assert season_offset == 2
def test_lookup_by_save_path_fallback(self, renamer, db_session):
"""Fourth priority: lookup by save_path when other methods fail."""
from module.database.bangumi import BangumiDatabase
from module.database.torrent import TorrentDatabase
from module.models import Bangumi
# Create bangumi with offsets and save_path
bangumi_db = BangumiDatabase(db_session)
bangumi = Bangumi(
official_title="Path Match Anime",
year="2024",
title_raw="unique_raw_that_wont_match",
season=1,
save_path="/downloads/Bangumi/Path Match Anime (2024)/Season 1",
episode_offset=10,
season_offset=-1,
)
bangumi_db.add(bangumi)
with patch("module.manager.renamer.Database") as MockDatabase:
mock_db = MagicMock()
mock_db.__enter__ = MagicMock(return_value=mock_db)
mock_db.__exit__ = MagicMock(return_value=False)
mock_db.torrent = TorrentDatabase(db_session)
mock_db.bangumi = BangumiDatabase(db_session)
MockDatabase.return_value = mock_db
episode_offset, season_offset = renamer._lookup_offsets(
torrent_hash="nonexistent_hash",
torrent_name="completely_different_name.mkv",
save_path="/downloads/Bangumi/Path Match Anime (2024)/Season 1",
tags="",
)
assert episode_offset == 10
assert season_offset == -1
def test_lookup_returns_zero_when_not_found(self, renamer, db_session):
"""Returns (0, 0) when no matching bangumi found."""
from module.database.bangumi import BangumiDatabase
from module.database.torrent import TorrentDatabase
with patch("module.manager.renamer.Database") as MockDatabase:
mock_db = MagicMock()
mock_db.__enter__ = MagicMock(return_value=mock_db)
mock_db.__exit__ = MagicMock(return_value=False)
mock_db.torrent = TorrentDatabase(db_session)
mock_db.bangumi = BangumiDatabase(db_session)
MockDatabase.return_value = mock_db
episode_offset, season_offset = renamer._lookup_offsets(
torrent_hash="nonexistent",
torrent_name="no_match",
save_path="/no/match/path",
tags="",
)
assert episode_offset == 0
assert season_offset == 0
def test_lookup_skips_deleted_bangumi(self, renamer, db_session):
"""Skips deleted bangumi even if hash/tag matches."""
from module.database.bangumi import BangumiDatabase
from module.database.torrent import TorrentDatabase
from module.models import Bangumi
# Create deleted bangumi
bangumi_db = BangumiDatabase(db_session)
bangumi = Bangumi(
official_title="Deleted Anime",
year="2024",
title_raw="deleted_raw",
season=1,
episode_offset=99,
season_offset=99,
deleted=True,
)
bangumi_db.add(bangumi)
with patch("module.manager.renamer.Database") as MockDatabase:
mock_db = MagicMock()
mock_db.__enter__ = MagicMock(return_value=mock_db)
mock_db.__exit__ = MagicMock(return_value=False)
mock_db.torrent = TorrentDatabase(db_session)
mock_db.bangumi = BangumiDatabase(db_session)
MockDatabase.return_value = mock_db
episode_offset, season_offset = renamer._lookup_offsets(
torrent_hash="nonexistent",
torrent_name="no_match",
save_path="/no/match",
tags=f"ab:{bangumi.id}",
)
# Should return (0, 0) because bangumi is deleted
assert episode_offset == 0
assert season_offset == 0
def test_lookup_handles_database_exception(self, renamer):
"""Returns (0, 0) when database throws exception."""
with patch("module.manager.renamer.Database") as MockDatabase:
MockDatabase.side_effect = Exception("Database connection failed")
episode_offset, season_offset = renamer._lookup_offsets(
torrent_hash="any",
torrent_name="any",
save_path="/any",
tags="",
)
assert episode_offset == 0
assert season_offset == 0