mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 03:56:43 +08:00
fix(search): disambiguate yearless same-title resources
This commit is contained in:
@@ -482,6 +482,13 @@ class TorrentHelper:
|
||||
str(int(mediainfo.year) + 1)]:
|
||||
logger.debug(f'{torrent.site_name} - {torrent.title} 年份不匹配 {mediainfo.year}')
|
||||
return False
|
||||
# 无年份同名剧仅靠标题无法消歧,先用目标剧已知的季集拓扑排除不可能候选。
|
||||
if mediainfo.type == MediaType.TV and not TorrentHelper._match_tv_topology(
|
||||
mediainfo=mediainfo,
|
||||
torrent_meta=torrent_meta,
|
||||
torrent=torrent,
|
||||
):
|
||||
return False
|
||||
# 比对标题和原语种标题
|
||||
if meta_names.intersection(media_titles):
|
||||
logger.info(f'{mediainfo.title} 通过标题匹配到资源:{torrent.site_name} - {torrent.title}')
|
||||
@@ -515,6 +522,168 @@ class TorrentHelper:
|
||||
logger.debug(f'{torrent.site_name} - {torrent.title} 标题不匹配,识别名称:{meta_names}')
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _match_tv_topology(
|
||||
mediainfo: MediaInfo,
|
||||
torrent_meta: MetaBase,
|
||||
torrent: TorrentInfo,
|
||||
) -> bool:
|
||||
"""用目标剧已知的季集范围排除无年份同名候选。"""
|
||||
begin_season = getattr(torrent_meta, "begin_season", None)
|
||||
end_season = getattr(torrent_meta, "end_season", None)
|
||||
if begin_season is None:
|
||||
torrent_seasons = set()
|
||||
elif end_season is None:
|
||||
torrent_seasons = {begin_season}
|
||||
else:
|
||||
start, end = sorted((begin_season, end_season))
|
||||
torrent_seasons = set(range(start, end + 1))
|
||||
|
||||
known_seasons = set()
|
||||
for season_map in (
|
||||
getattr(mediainfo, "seasons", None),
|
||||
getattr(mediainfo, "season_years", None),
|
||||
):
|
||||
for season in (season_map or {}).keys():
|
||||
try:
|
||||
known_seasons.add(int(season))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if not known_seasons:
|
||||
try:
|
||||
season_count = int(getattr(mediainfo, "number_of_seasons", None) or 0)
|
||||
except (TypeError, ValueError):
|
||||
season_count = 0
|
||||
if season_count:
|
||||
known_seasons = set(range(1, season_count + 1))
|
||||
|
||||
if torrent_seasons and known_seasons and not torrent_seasons.issubset(known_seasons):
|
||||
logger.debug(
|
||||
f'{torrent.site_name} - {torrent.title} 季范围 {sorted(torrent_seasons)} '
|
||||
f'超出目标媒体季范围 {sorted(known_seasons)}'
|
||||
)
|
||||
return False
|
||||
|
||||
if len(torrent_seasons) != 1:
|
||||
return True
|
||||
season = next(iter(torrent_seasons))
|
||||
target_episodes = (getattr(mediainfo, "seasons", None) or {}).get(season) or []
|
||||
if not target_episodes and len(known_seasons) == 1:
|
||||
try:
|
||||
episode_count = int(getattr(mediainfo, "number_of_episodes", None) or 0)
|
||||
except (TypeError, ValueError):
|
||||
episode_count = 0
|
||||
if episode_count:
|
||||
target_episodes = list(range(1, episode_count + 1))
|
||||
torrent_episodes = getattr(torrent_meta, "episode_list", None) or []
|
||||
if target_episodes and torrent_episodes and max(torrent_episodes) > max(target_episodes):
|
||||
logger.debug(
|
||||
f'{torrent.site_name} - {torrent.title} 最大集数 {max(torrent_episodes)} '
|
||||
f'超出目标媒体第 {season} 季总集数 {max(target_episodes)}'
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def requires_identity_disambiguation(
|
||||
mediainfo: MediaInfo,
|
||||
torrent_meta: MetaBase,
|
||||
) -> bool:
|
||||
"""判断无年份标题是否只通过目标别名命中,需进一步核验媒体身份。"""
|
||||
if (
|
||||
mediainfo.type != MediaType.TV
|
||||
or getattr(torrent_meta, "year", None)
|
||||
or not getattr(mediainfo, "year", None)
|
||||
):
|
||||
return False
|
||||
media_titles = {
|
||||
text_tools.normalize_upper(value)
|
||||
for value in (
|
||||
getattr(mediainfo, "title", None),
|
||||
getattr(mediainfo, "original_title", None),
|
||||
)
|
||||
if value
|
||||
}
|
||||
media_names = {
|
||||
text_tools.normalize_upper(value)
|
||||
for value in (getattr(mediainfo, "names", None) or [])
|
||||
if value
|
||||
}
|
||||
meta_names = {
|
||||
text_tools.normalize_upper(value)
|
||||
for value in (
|
||||
getattr(torrent_meta, "cn_name", None),
|
||||
getattr(torrent_meta, "en_name", None),
|
||||
)
|
||||
if value
|
||||
}
|
||||
return bool(meta_names.intersection(media_names)) and not bool(
|
||||
meta_names.intersection(media_titles)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def match_same_work_evidence(
|
||||
target_mediainfo: MediaInfo,
|
||||
candidate_mediainfo: MediaInfo,
|
||||
torrent_meta: MetaBase,
|
||||
) -> Tuple[bool, str]:
|
||||
"""判断媒体 ID 冲突时是否存在足以覆盖候选识别的同作品证据。"""
|
||||
target_identity = resolve_media_identity(media=target_mediainfo)
|
||||
candidate_identity = resolve_media_identity(media=candidate_mediainfo)
|
||||
if all(target_identity) and target_identity == candidate_identity:
|
||||
return True, f"媒体身份 {target_identity[0]}:{target_identity[1]}"
|
||||
|
||||
torrent_year = str(getattr(torrent_meta, "year", None) or "")[:4]
|
||||
target_years = {
|
||||
str(year)[:4]
|
||||
for year in (
|
||||
[getattr(target_mediainfo, "year", None)]
|
||||
+ list((getattr(target_mediainfo, "season_years", None) or {}).values())
|
||||
)
|
||||
if year
|
||||
}
|
||||
if torrent_year and torrent_year in target_years:
|
||||
return True, f"资源年份 {torrent_year}"
|
||||
|
||||
for field in ("imdb_id", "tvdb_id", "douban_id", "bangumi_id"):
|
||||
target_id = getattr(target_mediainfo, field, None)
|
||||
candidate_id = getattr(candidate_mediainfo, field, None)
|
||||
if target_id and candidate_id and str(target_id) == str(candidate_id):
|
||||
return True, f"共同 {field}"
|
||||
|
||||
target_year = str(getattr(target_mediainfo, "year", None) or "")[:4]
|
||||
candidate_year = str(getattr(candidate_mediainfo, "year", None) or "")[:4]
|
||||
if target_year and candidate_year and target_year != candidate_year:
|
||||
return False, f"首播年份冲突 {candidate_year} != {target_year}"
|
||||
|
||||
target_original_title = text_tools.normalize_upper(
|
||||
getattr(target_mediainfo, "original_title", None) or ""
|
||||
)
|
||||
candidate_original_title = text_tools.normalize_upper(
|
||||
getattr(candidate_mediainfo, "original_title", None) or ""
|
||||
)
|
||||
if (
|
||||
target_original_title
|
||||
and candidate_original_title
|
||||
and target_original_title != candidate_original_title
|
||||
):
|
||||
return False, "原始标题冲突"
|
||||
|
||||
target_language = str(
|
||||
getattr(target_mediainfo, "original_language", None) or ""
|
||||
).casefold()
|
||||
candidate_language = str(
|
||||
getattr(candidate_mediainfo, "original_language", None) or ""
|
||||
).casefold()
|
||||
if target_language and candidate_language and target_language != candidate_language:
|
||||
return False, f"原始语言冲突 {candidate_language} != {target_language}"
|
||||
|
||||
if target_year and candidate_year:
|
||||
return True, f"共同首播年份 {target_year}"
|
||||
if target_original_title and candidate_original_title:
|
||||
return True, "共同原始标题"
|
||||
return False, "资源无年份,候选与目标也没有可核验的共同元数据"
|
||||
|
||||
@staticmethod
|
||||
def filter_torrent(torrent_info: TorrentInfo,
|
||||
filter_params: Dict[str, Any]) -> bool:
|
||||
|
||||
@@ -1331,6 +1331,7 @@ class SearchChain(ChainBase):
|
||||
|
||||
# 开始匹配
|
||||
_match_torrents = []
|
||||
disambiguation_cache: Dict[Tuple[str, str, str], Optional[MediaInfo]] = {}
|
||||
try:
|
||||
# 英文标题应该在别名/原标题中,不需要再匹配
|
||||
logger.info(f"开始匹配结果 标题:{mediainfo.title},原标题:{mediainfo.original_title},别名:{mediainfo.names}")
|
||||
@@ -1370,6 +1371,38 @@ class SearchChain(ChainBase):
|
||||
if TorrentHelper.match_torrent(mediainfo=mediainfo,
|
||||
torrent_meta=torrent_meta,
|
||||
torrent=torrent):
|
||||
if TorrentHelper.requires_identity_disambiguation(
|
||||
mediainfo=mediainfo,
|
||||
torrent_meta=torrent_meta,
|
||||
):
|
||||
disambiguation_key = (
|
||||
torrent_meta.cn_name or "",
|
||||
torrent_meta.en_name or "",
|
||||
torrent_meta.year or "",
|
||||
)
|
||||
if disambiguation_key not in disambiguation_cache:
|
||||
disambiguation_cache[disambiguation_key] = MediaChain().recognize_by_meta(
|
||||
torrent_meta,
|
||||
obtain_images=False,
|
||||
)
|
||||
candidate_mediainfo = disambiguation_cache[disambiguation_key]
|
||||
if not candidate_mediainfo:
|
||||
logger.info(
|
||||
f'{torrent.site_name} - {torrent.title} '
|
||||
f'仅通过无年份别名命中且候选媒体身份无法确认,已跳过'
|
||||
)
|
||||
continue
|
||||
evidence_matched, evidence = TorrentHelper.match_same_work_evidence(
|
||||
target_mediainfo=mediainfo,
|
||||
candidate_mediainfo=candidate_mediainfo,
|
||||
torrent_meta=torrent_meta,
|
||||
)
|
||||
if not evidence_matched:
|
||||
logger.info(
|
||||
f'{torrent.site_name} - {torrent.title} '
|
||||
f'无年份同名候选未通过消歧:{evidence}'
|
||||
)
|
||||
continue
|
||||
# 匹配成功
|
||||
_match_torrents.append((torrent, torrent_meta, "title"))
|
||||
continue
|
||||
|
||||
+23
-1
@@ -2030,6 +2030,15 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
|
||||
if TorrentHelper.match_torrent(mediainfo=mediainfo,
|
||||
torrent_meta=torrent_meta,
|
||||
torrent=torrent_info):
|
||||
if TorrentHelper.requires_identity_disambiguation(
|
||||
mediainfo=mediainfo,
|
||||
torrent_meta=torrent_meta,
|
||||
):
|
||||
logger.info(
|
||||
f'{torrent_info.site_name} - {torrent_info.title} '
|
||||
f'仅通过无年份别名命中且候选媒体身份无法确认,已跳过'
|
||||
)
|
||||
continue
|
||||
# 匹配成功
|
||||
logger.info(
|
||||
f'{mediainfo.title_year} 通过标题匹配到可选资源:{torrent_info.site_name} - {torrent_info.title}')
|
||||
@@ -3839,13 +3848,26 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
|
||||
)
|
||||
return None
|
||||
|
||||
evidence_matched, evidence = TorrentHelper.match_same_work_evidence(
|
||||
target_mediainfo=target_mediainfo,
|
||||
candidate_mediainfo=candidate_mediainfo,
|
||||
torrent_meta=torrent_meta,
|
||||
)
|
||||
if not evidence_matched:
|
||||
logger.debug(
|
||||
f'{torrent_info.site_name} - {torrent_info.title} 候选媒体ID冲突且缺少同作品证据:'
|
||||
f'{evidence};{conflict_text}'
|
||||
)
|
||||
return None
|
||||
|
||||
context.media_info = target_mediainfo
|
||||
context.match_source = "title"
|
||||
context.candidate_recognized = False
|
||||
context.media_info_is_target = True
|
||||
logger.debug(
|
||||
f'{target_mediainfo.title_year} 候选媒体ID冲突({conflict_text}),'
|
||||
f'经标题或别名复核匹配到订阅目标:{torrent_info.site_name} - {torrent_info.title}'
|
||||
f'经标题或别名及{evidence}复核匹配到订阅目标:'
|
||||
f'{torrent_info.site_name} - {torrent_info.title}'
|
||||
)
|
||||
return target_mediainfo
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.domain.context import MediaInfo
|
||||
from app.domain.metainfo import MetaInfo, MetaInfoPath, find_metainfo
|
||||
from app.domain.meta.metabase import MetaBase
|
||||
from app.domain.meta.metamusic import MetaMusic
|
||||
@@ -160,6 +161,29 @@ def test_torrent_title_match_ignores_question_mark_variants():
|
||||
)
|
||||
|
||||
|
||||
def test_torrent_title_match_rejects_season_absent_from_target_series():
|
||||
"""无年份同名剧的资源季超出目标剧季范围时应拒绝。"""
|
||||
mediainfo = MediaInfo(
|
||||
title="家族计划",
|
||||
original_title="가족계획",
|
||||
names=["Family Matters"],
|
||||
type=MediaType.TV,
|
||||
year="2024",
|
||||
number_of_seasons=1,
|
||||
seasons={1: list(range(1, 7))},
|
||||
season_years={1: "2024"},
|
||||
)
|
||||
torrent_meta = MetaInfo("Family Matters S02 1080p WEBRip DD2.0 x264-TrollHD")
|
||||
torrent = SimpleNamespace(
|
||||
site_name="测试站点",
|
||||
title=torrent_meta.org_string,
|
||||
category=MediaType.TV.value,
|
||||
description=None,
|
||||
)
|
||||
|
||||
assert not TorrentHelper.match_torrent(mediainfo, torrent_meta, torrent)
|
||||
|
||||
|
||||
def test_python_metainfo_fallback_preserves_xxx_movie_title():
|
||||
"""Python 兜底解析不应删除合法 xXx 片名。"""
|
||||
with patch("app.adapters.system.rust.parse_metainfo", return_value=None):
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
from unittest.mock import Mock
|
||||
|
||||
from app.chain import search as search_module
|
||||
from app.chain.search import SearchChain
|
||||
from app.domain.context import MediaInfo, TorrentInfo
|
||||
from app.schemas.types import MediaSource, MediaType
|
||||
|
||||
|
||||
def test_exact_search_rejects_no_year_alias_recognized_as_different_work(monkeypatch):
|
||||
"""精确搜索中的无年份别名候选识别为不同作品时应拒绝。"""
|
||||
target = MediaInfo(
|
||||
media_source=MediaSource.TMDB,
|
||||
media_id="236356",
|
||||
tmdb_id=236356,
|
||||
title="家族计划",
|
||||
original_title="가족계획",
|
||||
names=["Family Matters"],
|
||||
type=MediaType.TV,
|
||||
year="2024",
|
||||
original_language="ko",
|
||||
season_years={1: "2024"},
|
||||
)
|
||||
candidate = MediaInfo(
|
||||
media_source=MediaSource.TMDB,
|
||||
media_id="30161",
|
||||
tmdb_id=30161,
|
||||
title="Family Matters",
|
||||
original_title="Family Matters",
|
||||
type=MediaType.TV,
|
||||
year="2008",
|
||||
original_language="en",
|
||||
)
|
||||
torrent = TorrentInfo(
|
||||
title="Family Matters S01 1080p WEBRip DD2.0 x264-TrollHD",
|
||||
site_name="测试站点",
|
||||
category=MediaType.TV.value,
|
||||
)
|
||||
media_chain = Mock()
|
||||
media_chain.recognize_by_meta.return_value = candidate
|
||||
monkeypatch.setattr(search_module, "MediaChain", Mock(return_value=media_chain))
|
||||
monkeypatch.setattr(
|
||||
search_module.TorrentHelper,
|
||||
"sort_torrents",
|
||||
staticmethod(lambda contexts: contexts),
|
||||
)
|
||||
chain = object.__new__(SearchChain)
|
||||
|
||||
contexts = chain._SearchChain__parse_result(
|
||||
torrents=[torrent],
|
||||
mediainfo=target,
|
||||
rule_groups=[],
|
||||
)
|
||||
|
||||
assert contexts == []
|
||||
media_chain.recognize_by_meta.assert_called_once()
|
||||
|
||||
|
||||
def test_exact_search_reuses_disambiguation_for_same_parsed_title(monkeypatch):
|
||||
"""同一解析标题的多条资源应复用一次候选识别,避免重复外部查询。"""
|
||||
target = MediaInfo(
|
||||
media_source=MediaSource.TMDB,
|
||||
media_id="236356",
|
||||
tmdb_id=236356,
|
||||
title="家族计划",
|
||||
original_title="가족계획",
|
||||
names=["Family Matters"],
|
||||
type=MediaType.TV,
|
||||
year="2024",
|
||||
season_years={1: "2024"},
|
||||
)
|
||||
candidate = MediaInfo(
|
||||
media_source=MediaSource.TMDB,
|
||||
media_id="236356",
|
||||
tmdb_id=236356,
|
||||
title="家族计划",
|
||||
type=MediaType.TV,
|
||||
year="2024",
|
||||
)
|
||||
torrents = [
|
||||
TorrentInfo(
|
||||
title=f"Family Matters S01 1080p WEB-DL {group}",
|
||||
site_name=f"测试站点{index}",
|
||||
category=MediaType.TV.value,
|
||||
)
|
||||
for index, group in enumerate(("GROUP-A", "GROUP-B"), start=1)
|
||||
]
|
||||
media_chain = Mock()
|
||||
media_chain.recognize_by_meta.return_value = candidate
|
||||
monkeypatch.setattr(search_module, "MediaChain", Mock(return_value=media_chain))
|
||||
monkeypatch.setattr(
|
||||
search_module.TorrentHelper,
|
||||
"sort_torrents",
|
||||
staticmethod(lambda contexts: contexts),
|
||||
)
|
||||
chain = object.__new__(SearchChain)
|
||||
|
||||
contexts = chain._SearchChain__parse_result(
|
||||
torrents=torrents,
|
||||
mediainfo=target,
|
||||
rule_groups=[],
|
||||
)
|
||||
|
||||
assert len(contexts) == 2
|
||||
media_chain.recognize_by_meta.assert_called_once()
|
||||
@@ -5,36 +5,44 @@ from app.domain.context import Context, MediaInfo, TorrentInfo
|
||||
from app.schemas.types import MediaSource, MediaType
|
||||
|
||||
|
||||
def _target_media(tmdb_id=106449, douban_id=None) -> MediaInfo:
|
||||
def _target_media(tmdb_id=106449, douban_id=None, **kwargs) -> MediaInfo:
|
||||
"""构造订阅目标媒体。"""
|
||||
media_source = MediaSource.TMDB if tmdb_id is not None else MediaSource.Douban
|
||||
media_id = str(tmdb_id) if tmdb_id is not None else douban_id
|
||||
defaults = {
|
||||
"title": "凡人修仙传",
|
||||
"original_title": "凡人修仙传",
|
||||
"names": ["A Record Of A Mortals Journey To Immortality"],
|
||||
"type": MediaType.TV,
|
||||
"year": "2020",
|
||||
"season_years": {1: "2020"},
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
return MediaInfo(
|
||||
media_source=media_source if media_id is not None else None,
|
||||
media_id=media_id,
|
||||
title="凡人修仙传",
|
||||
original_title="凡人修仙传",
|
||||
names=["A Record Of A Mortals Journey To Immortality"],
|
||||
type=MediaType.TV,
|
||||
year="2020",
|
||||
season_years={1: "2020"},
|
||||
tmdb_id=tmdb_id,
|
||||
douban_id=douban_id,
|
||||
**defaults,
|
||||
)
|
||||
|
||||
|
||||
def _candidate_media(tmdb_id=285479, douban_id=None) -> MediaInfo:
|
||||
def _candidate_media(tmdb_id=285479, douban_id=None, **kwargs) -> MediaInfo:
|
||||
"""构造由 RSS 标题推断出的候选媒体。"""
|
||||
media_source = MediaSource.TMDB if tmdb_id is not None else MediaSource.Douban
|
||||
media_id = str(tmdb_id) if tmdb_id is not None else douban_id
|
||||
defaults = {
|
||||
"title": "凡人修仙传",
|
||||
"type": MediaType.TV,
|
||||
"year": "2020",
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
return MediaInfo(
|
||||
media_source=media_source if media_id is not None else None,
|
||||
media_id=media_id,
|
||||
title="凡人修仙传",
|
||||
type=MediaType.TV,
|
||||
year="2020",
|
||||
tmdb_id=tmdb_id,
|
||||
douban_id=douban_id,
|
||||
**defaults,
|
||||
)
|
||||
|
||||
|
||||
@@ -96,6 +104,69 @@ def test_inferred_tmdb_conflict_falls_back_to_strict_alias_match():
|
||||
assert context.media_info_is_target is True
|
||||
|
||||
|
||||
def test_no_year_alias_rejects_candidate_with_different_first_air_year():
|
||||
"""无年份别名命中另一个首播年份的同名作品时应拒绝。"""
|
||||
target = _target_media(
|
||||
tmdb_id=236356,
|
||||
title="家族计划",
|
||||
original_title="가족계획",
|
||||
names=["Family Matters"],
|
||||
year="2024",
|
||||
original_language="ko",
|
||||
season_years={1: "2024"},
|
||||
)
|
||||
candidate = _candidate_media(
|
||||
tmdb_id=30161,
|
||||
title="Family Matters",
|
||||
original_title="Family Matters",
|
||||
year="2008",
|
||||
original_language="en",
|
||||
)
|
||||
meta = _torrent_meta(en_name="Family Matters")
|
||||
torrent = TorrentInfo(
|
||||
title="Family Matters S01 1080p WEBRip DD2.0 x264-TrollHD",
|
||||
site_name="测试站点",
|
||||
category=MediaType.TV.value,
|
||||
)
|
||||
context = _context(meta, candidate, torrent)
|
||||
|
||||
assert _reconcile(target, candidate, meta, torrent, context) is None
|
||||
assert context.media_info is candidate
|
||||
assert context.match_source == "tmdbid"
|
||||
|
||||
|
||||
def test_explicit_target_year_can_override_wrong_same_name_candidate():
|
||||
"""资源明确携带目标年份时应允许纠正同名候选的错误识别。"""
|
||||
target = _target_media(
|
||||
tmdb_id=236356,
|
||||
title="家族计划",
|
||||
original_title="가족계획",
|
||||
names=["Family Matters"],
|
||||
year="2024",
|
||||
original_language="ko",
|
||||
season_years={1: "2024"},
|
||||
)
|
||||
candidate = _candidate_media(
|
||||
tmdb_id=30161,
|
||||
title="Family Matters",
|
||||
original_title="Family Matters",
|
||||
year="2008",
|
||||
original_language="en",
|
||||
)
|
||||
meta = _torrent_meta(en_name="Family Matters")
|
||||
meta.year = "2024"
|
||||
torrent = TorrentInfo(
|
||||
title="Family Matters 2024 S01 1080p WEB-DL",
|
||||
site_name="测试站点",
|
||||
category=MediaType.TV.value,
|
||||
)
|
||||
context = _context(meta, candidate, torrent)
|
||||
|
||||
assert _reconcile(target, candidate, meta, torrent, context) is target
|
||||
assert context.media_info is target
|
||||
assert context.match_source == "title"
|
||||
|
||||
|
||||
def test_inferred_tmdb_conflict_rejects_nonmatching_title():
|
||||
"""ID 冲突且标题和别名不匹配时应继续拒绝候选。"""
|
||||
target = _target_media()
|
||||
|
||||
Reference in New Issue
Block a user