fix(bangumi): inherit year for same-title entries across subtitle groups

When a new bangumi entry is created for an already-known show (same
official_title, different group/quality so no semantic merge), it now
inherits the existing entry's year. The save path is built as
`title (year)/Season N`, so a Mikan-parsed entry (which never fills
year) previously diverged into a separate library folder from a
TMDB-parsed sibling that had one. `_inherit_year` in add()/add_all()
copies the year from any non-deleted same-title row when the incoming
entry has none; a parsed year is never overwritten.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
This commit is contained in:
Estrella Pan
2026-07-06 09:42:58 +02:00
parent 1dc1eb01b4
commit 2cdf146f91
2 changed files with 131 additions and 10 deletions

View File

@@ -126,6 +126,21 @@ def _find_semantic_match(data: Bangumi, candidates: list[Bangumi]) -> Optional[B
return None
def _inherit_year(data: Bangumi, candidates: list[Bangumi]) -> None:
"""新条目缺年份时继承同名已有条目的年份。
save path 由 ``official_title (year)`` 组成(downloader/path.py),同一部
番剧的不同订阅条目(不同字幕组/清晰度)若年份不一致,会被拆进两个媒体库
目录。Mikan 解析路径不填 year,因此从已有同名条目补齐。
"""
if data.year:
return
for candidate in candidates:
if candidate.year:
data.year = candidate.year
return
def match_bangumi_in_list(
torrent_name: str, bangumi_list: list[Bangumi]
) -> Optional[Bangumi]:
@@ -152,6 +167,17 @@ class BangumiDatabase:
def __init__(self, session: AsyncSession):
self.session = session
async def _same_title_candidates(self, official_title: str) -> list[Bangumi]:
"""加载与给定 official_title 同名的所有未删除条目。"""
statement = select(Bangumi).where(
and_(
Bangumi.official_title == official_title,
Bangumi.deleted == false(),
)
)
result = await self.session.execute(statement)
return list(result.scalars().all())
async def find_semantic_duplicate(self, data: Bangumi) -> Optional[Bangumi]:
"""
Find existing bangumi that semantically matches the new one.
@@ -166,15 +192,7 @@ class BangumiDatabase:
Returns the matching Bangumi if found, None otherwise.
"""
statement = select(Bangumi).where(
and_(
Bangumi.official_title == data.official_title,
Bangumi.deleted == false(),
)
)
result = await self.session.execute(statement)
candidates = list(result.scalars().all())
candidates = await self._same_title_candidates(data.official_title)
match = _find_semantic_match(data, candidates)
if match:
logger.debug(
@@ -249,7 +267,8 @@ class BangumiDatabase:
return False
# Check for semantic duplicate (same anime, different naming pattern)
semantic_match = await self.find_semantic_duplicate(data)
candidates = await self._same_title_candidates(data.official_title)
semantic_match = _find_semantic_match(data, candidates)
if semantic_match:
# Add as alias instead of creating new entry
await self.add_title_alias(semantic_match.id, data.title_raw)
@@ -259,6 +278,7 @@ class BangumiDatabase:
)
return False # Return False since we didn't add a new entry
_inherit_year(data, candidates)
self.session.add(data)
await self.session.commit()
logger.debug("Insert %s into database.", data.official_title)
@@ -328,6 +348,7 @@ class BangumiDatabase:
f"'{semantic_match.title_raw}' (official: {d.official_title})"
)
else:
_inherit_year(d, candidates_by_title.get(d.official_title, []))
really_to_add.append(d)
# Also deduplicate within the batch itself

View File

@@ -455,6 +455,106 @@ async def test_add_with_semantic_duplicate_creates_alias(db_session):
assert "Frieren Beyond Journey's End" in aliases
async def test_add_same_title_new_entry_inherits_year(db_session):
"""A new entry for an already-known anime (same official_title, different
group/quality so no semantic merge) must inherit the existing year, so both
entries build the same save path (title (year)/Season N)."""
db = BangumiDatabase(db_session)
existing = Bangumi(
official_title="躲在超市后门抽烟的两人",
year="2026",
title_raw="躲在超市后门抽烟的两人",
group_name="樱都字幕组",
dpi="1080p",
source="Baha",
subtitle="CHS",
rss_link="test1",
)
await db.add(existing)
new_entry = Bangumi(
official_title="躲在超市后门抽烟的两人",
year=None,
title_raw="Super no Ura de Yani Suu Futari",
group_name="Dynamis One",
dpi="1080p",
source="ABEMA",
subtitle="JPSC",
rss_link="test2",
)
result = await db.add(new_entry)
assert result is True # different group/source: a new entry, not a merge
assert new_entry.year == "2026"
async def test_add_same_title_keeps_own_year(db_session):
"""An explicitly parsed year must not be overwritten by an existing entry's."""
db = BangumiDatabase(db_session)
await db.add(
Bangumi(
official_title="Frieren",
year="2023",
title_raw="Sousou no Frieren",
group_name="LoliHouse",
dpi="1080p",
source="Web",
subtitle="CHT",
rss_link="test1",
)
)
new_entry = Bangumi(
official_title="Frieren",
year="2024",
title_raw="Frieren S2",
group_name="OtherGroup",
dpi="720p",
source="Baha",
subtitle="CHS",
rss_link="test2",
)
result = await db.add(new_entry)
assert result is True
assert new_entry.year == "2024"
async def test_add_all_same_title_new_entries_inherit_year(db_session):
"""add_all() applies the same year inheritance as add()."""
db = BangumiDatabase(db_session)
await db.add(
Bangumi(
official_title="躲在超市后门抽烟的两人",
year="2026",
title_raw="躲在超市后门抽烟的两人",
group_name="樱都字幕组",
dpi="1080p",
source="Baha",
subtitle="CHS",
rss_link="test1",
)
)
new_entry = Bangumi(
official_title="躲在超市后门抽烟的两人",
year=None,
title_raw="Super no Ura de Yani Suu Futari",
group_name="Dynamis One",
dpi="1080p",
source="ABEMA",
subtitle="JPSC",
rss_link="test2",
)
added = await db.add_all([new_entry])
assert added == 1
assert new_entry.year == "2026"
async def test_add_all_semantic_duplicates_across_batch_all_merged_as_aliases(
db_session,
):