Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend/src/module/database/bangumi.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,10 @@ def search_id(self, _id: int) -> Optional[Bangumi]:
logger.debug("[Database] Find bangumi id: %s.", _id)
return bangumi

def search_official_title(self, official_title: str) -> Optional[Bangumi]:
statement = select(Bangumi).where(Bangumi.official_title == official_title)
return self.session.execute(statement).scalar_one_or_none()

def search_ids(self, ids: list[int]) -> list[Bangumi]:
"""Batch lookup multiple bangumi by their IDs."""
if not ids:
Expand Down
7 changes: 4 additions & 3 deletions backend/src/module/parser/analyser/raw_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
SUB_RE = re.compile(r"[简繁日字幕]|CH|BIG5|GB")

FALLBACK_EP_PATTERNS = [
re.compile(r" (\d+) ?(?=\[)"), # #876/#910: digits before [
re.compile(r"\[(\d+)\(\d+\)\]"), # #773: [02(57)]
re.compile(r" (\d+) ?(?=\[)"), # #876/#910: digits before [
re.compile(r"\[(\d+)\(\d+\)\]"), # #773: [02(57)]
]

PREFIX_RE = re.compile(r"[^\w\s\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff-]")
Expand All @@ -32,6 +32,7 @@ def _fallback_parse(content_title: str) -> tuple | None:
return season_info, episode_info, other
return None


CHINESE_NUMBER_MAP = {
"一": 1,
"二": 2,
Expand Down Expand Up @@ -201,7 +202,7 @@ def process(raw_title: str):
def raw_parser(raw: str) -> Episode | None:
ret = process(raw)
if ret is None:
logger.error(f"Parser cannot analyse {raw}")
logger.info(f"Detected non-episodic resource: {raw}, skipping.")
return None
name_en, name_zh, name_jp, season, sr, episode, sub, dpi, source, group = ret
return Episode(
Expand Down
2 changes: 2 additions & 0 deletions backend/src/module/parser/title_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ def raw_parser(raw: str) -> Bangumi | None:
episode = Episode(**episode_dict)
else:
episode = raw_parser(raw)
if episode is None:
return None

titles = {
"zh": episode.title_zh,
Expand Down
129 changes: 114 additions & 15 deletions backend/src/test/test_issue_bugs.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,46 +92,69 @@ class TestIssue977EpisodeZeroOffset:
def test_episode_zero_preserved_with_no_offset(self):
"""Episode 0 with offset=0 stays as E00."""
ep = EpisodeFile(
media_path="old.mkv", title="Fate strange Fake", season=1,
episode=0, suffix=".mkv",
media_path="old.mkv",
title="Fate strange Fake",
season=1,
episode=0,
suffix=".mkv",
)
result = Renamer.gen_path(
ep, "Fate strange Fake", method="pn", episode_offset=0
)
result = Renamer.gen_path(ep, "Fate strange Fake", method="pn", episode_offset=0)
assert "E00" in result

def test_episode_zero_immune_to_positive_offset(self):
"""Episode 0 (special/OVA) should not be shifted by positive offset."""
ep = EpisodeFile(
media_path="old.mkv", title="Fate strange Fake", season=1,
episode=0, suffix=".mkv",
media_path="old.mkv",
title="Fate strange Fake",
season=1,
episode=0,
suffix=".mkv",
)
result = Renamer.gen_path(
ep, "Fate strange Fake", method="pn", episode_offset=1
)
result = Renamer.gen_path(ep, "Fate strange Fake", method="pn", episode_offset=1)
assert "E00" in result

def test_episode_zero_immune_to_negative_offset(self):
"""Episode 0 (special/OVA) should not be shifted by negative offset."""
ep = EpisodeFile(
media_path="old.mkv", title="Fate strange Fake", season=1,
episode=0, suffix=".mkv",
media_path="old.mkv",
title="Fate strange Fake",
season=1,
episode=0,
suffix=".mkv",
)
result = Renamer.gen_path(
ep, "Fate strange Fake", method="pn", episode_offset=-12
)
result = Renamer.gen_path(ep, "Fate strange Fake", method="pn", episode_offset=-12)
assert "E00" in result

def test_regular_episode_offset_still_works(self):
"""Regular episodes should still be affected by offset normally."""
ep = EpisodeFile(
media_path="old.mkv", title="Test", season=1,
episode=13, suffix=".mkv",
media_path="old.mkv",
title="Test",
season=1,
episode=13,
suffix=".mkv",
)
result = Renamer.gen_path(ep, "Test", method="pn", episode_offset=-12)
assert "E01" in result # 13 - 12 = 1

def test_episode_zero_advance_method(self):
"""Episode 0 with advance method and no offset stays E00."""
ep = EpisodeFile(
media_path="old.mkv", title="Test", season=1,
episode=0, suffix=".mkv",
media_path="old.mkv",
title="Test",
season=1,
episode=0,
suffix=".mkv",
)
result = Renamer.gen_path(
ep, "Bangumi Name", method="advance", episode_offset=0
)
result = Renamer.gen_path(ep, "Bangumi Name", method="advance", episode_offset=0)
assert result == "Bangumi Name S01E00.mkv"


Expand Down Expand Up @@ -304,7 +327,9 @@ def test_engine_caches_filter_pattern(self):
class TestIssue990NumberPrefixTitle:
"""Issue #990: Titles starting with numbers crash RSS loop."""

PROBLEM_TITLE = "[ANi] 29 岁单身中坚冒险家的日常 - 07 [1080P][Baha][WEB-DL][AAC AVC][CHT][MP4]"
PROBLEM_TITLE = (
"[ANi] 29 岁单身中坚冒险家的日常 - 07 [1080P][Baha][WEB-DL][AAC AVC][CHT][MP4]"
)

def test_raw_parser_correctly_parses_leading_number_title(self):
"""raw_parser correctly parses title starting with number and extracts episode."""
Expand Down Expand Up @@ -458,3 +483,77 @@ def test_match_list_no_crash_on_corrupted_data(self, db_session):

# Should not crash even with corrupted data in the DB
unmatched = db.match_list([torrent], "https://mikanani.me/RSS/test")


# ---------------------------------------------------------------------------
# Issue #992: Non-episodic resource causes AttributeError in title_parser
# https://github.com/EstrellaXD/Auto_Bangumi/issues/992
#
# When raw_parser returns None (movie/collection resources), title_parser
# accesses episode.title_zh on None, causing AttributeError.
# ---------------------------------------------------------------------------


class TestIssue992NonEpisodicAttributeError:
"""Issue #992: title_parser crashes on non-episodic resources."""

# Titles that raw_parser cannot parse (returns None)
NON_EPISODIC_TITLES = [
"[阿特拉斯字幕组·雪原市出差所][命运-奇异赝品_Fate/strange Fake][04_半神们的卡农曲][简繁日内封PGS][日语配音版_Japanese Dub][Web-DL Remux][1080p AVC AAC]",
"[KitaujiSub] Shikanoko Nokonoko Koshitantan [01Pre][WebRip][HEVC_AAC][CHS_JP].mp4",
]

@pytest.mark.parametrize("title", NON_EPISODIC_TITLES)
def test_title_parser_returns_none_for_non_episodic(self, title):
"""TitleParser.raw_parser should return None instead of crashing."""
from module.parser.title_parser import TitleParser

result = TitleParser.raw_parser(title)
assert result is None

def test_raw_parser_returns_none_for_unparseable(self):
"""raw_parser returns None for resources it cannot parse."""
result = raw_parser(self.NON_EPISODIC_TITLES[0])
assert result is None


# ---------------------------------------------------------------------------
# Issue #1005: BangumiDatabase missing search_official_title method
# https://github.com/EstrellaXD/Auto_Bangumi/issues/1005
# ---------------------------------------------------------------------------


class TestIssue1005SearchOfficialTitle:
"""Issue #1005: search_official_title method must exist on BangumiDatabase."""

def test_method_exists(self):
"""BangumiDatabase should have search_official_title method."""
from module.database.bangumi import BangumiDatabase

assert hasattr(BangumiDatabase, "search_official_title")

def test_search_official_title_finds_match(self, db_session):
"""search_official_title returns the matching bangumi."""
from module.database.bangumi import BangumiDatabase
from module.models import Bangumi

db = BangumiDatabase(db_session)
bangumi = Bangumi(
official_title="路人女主的养成方法",
title_raw="Saenai Heroine no Sodatekata",
season=1,
rss_link="test",
)
db.add(bangumi)

result = db.search_official_title("路人女主的养成方法")
assert result is not None
assert result.official_title == "路人女主的养成方法"

def test_search_official_title_returns_none_when_not_found(self, db_session):
"""search_official_title returns None for non-existent title."""
from module.database.bangumi import BangumiDatabase

db = BangumiDatabase(db_session)
result = db.search_official_title("不存在的番剧")
assert result is None