mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-07 08:26:53 +08:00
fix: preserve subscribe season zero targets (#5983)
This commit is contained in:
@@ -80,7 +80,8 @@ async def create_subscribe(
|
|||||||
if subscribe_in.doubanid or subscribe_in.bangumiid:
|
if subscribe_in.doubanid or subscribe_in.bangumiid:
|
||||||
meta = MetaInfo(subscribe_in.name)
|
meta = MetaInfo(subscribe_in.name)
|
||||||
subscribe_in.name = meta.name
|
subscribe_in.name = meta.name
|
||||||
subscribe_in.season = meta.begin_season
|
if subscribe_in.season is None:
|
||||||
|
subscribe_in.season = meta.begin_season
|
||||||
# 标题转换
|
# 标题转换
|
||||||
if subscribe_in.name:
|
if subscribe_in.name:
|
||||||
title = subscribe_in.name
|
title = subscribe_in.name
|
||||||
|
|||||||
+20
-24
@@ -45,6 +45,17 @@ from app.schemas.types import MediaType, SystemConfigKey, MessageChannel, Notifi
|
|||||||
subscribe_interaction_manager = SlashInteractionManager()
|
subscribe_interaction_manager = SlashInteractionManager()
|
||||||
|
|
||||||
|
|
||||||
|
def build_subscribe_meta(subscribe: Subscribe) -> MetaBase:
|
||||||
|
"""
|
||||||
|
按订阅对象构造主程序链路共用的 MetaInfo。
|
||||||
|
"""
|
||||||
|
meta = MetaInfo(subscribe.name)
|
||||||
|
meta.year = subscribe.year
|
||||||
|
meta.begin_season = subscribe.season
|
||||||
|
meta.type = MediaType(subscribe.type)
|
||||||
|
return meta
|
||||||
|
|
||||||
|
|
||||||
class SubscribeChain(ChainBase):
|
class SubscribeChain(ChainBase):
|
||||||
"""
|
"""
|
||||||
订阅管理处理链
|
订阅管理处理链
|
||||||
@@ -1013,12 +1024,8 @@ class SubscribeChain(ChainBase):
|
|||||||
time.sleep(sleep_time)
|
time.sleep(sleep_time)
|
||||||
try:
|
try:
|
||||||
logger.info(f'开始搜索订阅,标题:{subscribe.name} ...')
|
logger.info(f'开始搜索订阅,标题:{subscribe.name} ...')
|
||||||
# 生成元数据
|
|
||||||
meta = MetaInfo(subscribe.name)
|
|
||||||
meta.year = subscribe.year
|
|
||||||
meta.begin_season = subscribe.season if subscribe.season is not None else None
|
|
||||||
try:
|
try:
|
||||||
meta.type = MediaType(subscribe.type)
|
meta = build_subscribe_meta(subscribe)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logger.error(f'订阅 {subscribe.name} 类型错误:{subscribe.type}')
|
logger.error(f'订阅 {subscribe.name} 类型错误:{subscribe.type}')
|
||||||
continue
|
continue
|
||||||
@@ -1408,12 +1415,8 @@ class SubscribeChain(ChainBase):
|
|||||||
break
|
break
|
||||||
logger.info(f'开始匹配订阅,标题:{subscribe.name} ...')
|
logger.info(f'开始匹配订阅,标题:{subscribe.name} ...')
|
||||||
mediakey = subscribe.tmdbid or subscribe.doubanid
|
mediakey = subscribe.tmdbid or subscribe.doubanid
|
||||||
# 生成元数据
|
|
||||||
meta = MetaInfo(subscribe.name)
|
|
||||||
meta.year = subscribe.year
|
|
||||||
meta.begin_season = subscribe.season or None
|
|
||||||
try:
|
try:
|
||||||
meta.type = MediaType(subscribe.type)
|
meta = build_subscribe_meta(subscribe)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logger.error(f'订阅 {subscribe.name} 类型错误:{subscribe.type}')
|
logger.error(f'订阅 {subscribe.name} 类型错误:{subscribe.type}')
|
||||||
continue
|
continue
|
||||||
@@ -1558,7 +1561,7 @@ class SubscribeChain(ChainBase):
|
|||||||
logger.debug(f'{torrent_info.title} 有多季,不处理')
|
logger.debug(f'{torrent_info.title} 有多季,不处理')
|
||||||
continue
|
continue
|
||||||
# 比对季
|
# 比对季
|
||||||
if torrent_meta.begin_season:
|
if torrent_meta.begin_season is not None:
|
||||||
if meta.begin_season != torrent_meta.begin_season:
|
if meta.begin_season != torrent_meta.begin_season:
|
||||||
logger.debug(f'{torrent_info.title} 季不匹配')
|
logger.debug(f'{torrent_info.title} 季不匹配')
|
||||||
continue
|
continue
|
||||||
@@ -1706,12 +1709,8 @@ class SubscribeChain(ChainBase):
|
|||||||
if global_vars.is_system_stopped:
|
if global_vars.is_system_stopped:
|
||||||
break
|
break
|
||||||
logger.info(f'开始更新订阅元数据:{subscribe.name} ...')
|
logger.info(f'开始更新订阅元数据:{subscribe.name} ...')
|
||||||
# 生成元数据
|
|
||||||
meta = MetaInfo(subscribe.name)
|
|
||||||
meta.year = subscribe.year
|
|
||||||
meta.begin_season = subscribe.season or None
|
|
||||||
try:
|
try:
|
||||||
meta.type = MediaType(subscribe.type)
|
meta = build_subscribe_meta(subscribe)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logger.error(f'订阅 {subscribe.name} 类型错误:{subscribe.type}')
|
logger.error(f'订阅 {subscribe.name} 类型错误:{subscribe.type}')
|
||||||
continue
|
continue
|
||||||
@@ -1825,7 +1824,8 @@ class SubscribeChain(ChainBase):
|
|||||||
if subscribe_in.doubanid or subscribe_in.bangumiid:
|
if subscribe_in.doubanid or subscribe_in.bangumiid:
|
||||||
meta = MetaInfo(subscribe_in.name)
|
meta = MetaInfo(subscribe_in.name)
|
||||||
subscribe_in.name = meta.name
|
subscribe_in.name = meta.name
|
||||||
subscribe_in.season = meta.begin_season
|
if subscribe_in.season is None:
|
||||||
|
subscribe_in.season = meta.begin_season
|
||||||
# 标题转换
|
# 标题转换
|
||||||
if subscribe_in.name:
|
if subscribe_in.name:
|
||||||
title = subscribe_in.name
|
title = subscribe_in.name
|
||||||
@@ -2553,7 +2553,7 @@ class SubscribeChain(ChainBase):
|
|||||||
"""
|
"""
|
||||||
if subscribe.type == MediaType.MOVIE.value:
|
if subscribe.type == MediaType.MOVIE.value:
|
||||||
return "电影"
|
return "电影"
|
||||||
season = subscribe.season or 1
|
season = subscribe.season if subscribe.season is not None else 1
|
||||||
if subscribe.total_episode:
|
if subscribe.total_episode:
|
||||||
lack_episode = (
|
lack_episode = (
|
||||||
subscribe.lack_episode
|
subscribe.lack_episode
|
||||||
@@ -3056,12 +3056,8 @@ class SubscribeChain(ChainBase):
|
|||||||
else:
|
else:
|
||||||
episodes[0].download.append(file_info)
|
episodes[0].download.append(file_info)
|
||||||
|
|
||||||
# 生成元数据
|
|
||||||
meta = MetaInfo(subscribe.name)
|
|
||||||
meta.year = subscribe.year
|
|
||||||
meta.begin_season = subscribe.season or None
|
|
||||||
try:
|
try:
|
||||||
meta.type = MediaType(subscribe.type)
|
meta = build_subscribe_meta(subscribe)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logger.error(f'订阅 {subscribe.name} 类型错误:{subscribe.type}')
|
logger.error(f'订阅 {subscribe.name} 类型错误:{subscribe.type}')
|
||||||
return subscribe_info
|
return subscribe_info
|
||||||
@@ -3156,7 +3152,7 @@ class SubscribeChain(ChainBase):
|
|||||||
|
|
||||||
if not subscribe.best_version:
|
if not subscribe.best_version:
|
||||||
totals = {}
|
totals = {}
|
||||||
if subscribe.season and effective_total_episode:
|
if subscribe.season is not None and effective_total_episode:
|
||||||
totals = {
|
totals = {
|
||||||
subscribe.season: effective_total_episode
|
subscribe.season: effective_total_episode
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,20 +179,20 @@ def _build_meta_info(
|
|||||||
if metainfo.get('doubanid'):
|
if metainfo.get('doubanid'):
|
||||||
meta.doubanid = metainfo['doubanid']
|
meta.doubanid = metainfo['doubanid']
|
||||||
if metainfo.get('type'):
|
if metainfo.get('type'):
|
||||||
meta.type = metainfo['type']
|
meta.type = MediaType(metainfo['type']) if isinstance(metainfo['type'], str) else metainfo['type']
|
||||||
if metainfo.get('episode_group'):
|
if metainfo.get('episode_group'):
|
||||||
meta.episode_group = metainfo['episode_group']
|
meta.episode_group = metainfo['episode_group']
|
||||||
if metainfo.get('begin_season'):
|
if metainfo.get('begin_season') is not None:
|
||||||
meta.begin_season = metainfo['begin_season']
|
meta.begin_season = metainfo['begin_season']
|
||||||
if metainfo.get('end_season'):
|
if metainfo.get('end_season') is not None:
|
||||||
meta.end_season = metainfo['end_season']
|
meta.end_season = metainfo['end_season']
|
||||||
if metainfo.get('total_season'):
|
if metainfo.get('total_season') is not None:
|
||||||
meta.total_season = metainfo['total_season']
|
meta.total_season = metainfo['total_season']
|
||||||
if metainfo.get('begin_episode'):
|
if metainfo.get('begin_episode') is not None:
|
||||||
meta.begin_episode = metainfo['begin_episode']
|
meta.begin_episode = metainfo['begin_episode']
|
||||||
if metainfo.get('end_episode'):
|
if metainfo.get('end_episode') is not None:
|
||||||
meta.end_episode = metainfo['end_episode']
|
meta.end_episode = metainfo['end_episode']
|
||||||
if metainfo.get('total_episode'):
|
if metainfo.get('total_episode') is not None:
|
||||||
meta.total_episode = metainfo['total_episode']
|
meta.total_episode = metainfo['total_episode']
|
||||||
return meta
|
return meta
|
||||||
|
|
||||||
|
|||||||
@@ -1050,7 +1050,7 @@ class DoubanModule(_ModuleBase):
|
|||||||
continue
|
continue
|
||||||
if mtype and mtype.value != type_name:
|
if mtype and mtype.value != type_name:
|
||||||
continue
|
continue
|
||||||
if mtype and mtype == MediaType.TV and not season:
|
if mtype and mtype == MediaType.TV and season is None:
|
||||||
season = 1
|
season = 1
|
||||||
item = item_obj.get("target")
|
item = item_obj.get("target")
|
||||||
title = item.get("title")
|
title = item.get("title")
|
||||||
@@ -1059,9 +1059,9 @@ class DoubanModule(_ModuleBase):
|
|||||||
meta = MetaInfo(title)
|
meta = MetaInfo(title)
|
||||||
if type_name == MediaType.TV.value:
|
if type_name == MediaType.TV.value:
|
||||||
meta.type = MediaType.TV
|
meta.type = MediaType.TV
|
||||||
meta.begin_season = meta.begin_season or 1
|
meta.begin_season = meta.begin_season if meta.begin_season is not None else 1
|
||||||
if meta.name == name \
|
if meta.name == name \
|
||||||
and ((not season and not meta.begin_season) or meta.begin_season == season) \
|
and ((season is None and meta.begin_season is None) or meta.begin_season == season) \
|
||||||
and (not year or item.get('year') == year):
|
and (not year or item.get('year') == year):
|
||||||
logger.info(f"{name} 匹配到豆瓣信息:{item.get('id')} {item.get('title')}")
|
logger.info(f"{name} 匹配到豆瓣信息:{item.get('id')} {item.get('title')}")
|
||||||
return item
|
return item
|
||||||
|
|||||||
@@ -219,3 +219,38 @@ class MediaRecognizeModulesTest(TestCase):
|
|||||||
self.assertEqual(len(result), 1)
|
self.assertEqual(len(result), 1)
|
||||||
self.assertEqual(result[0].title, "测试剧 第二季")
|
self.assertEqual(result[0].title, "测试剧 第二季")
|
||||||
self.assertEqual(result[0].season, 2)
|
self.assertEqual(result[0].season, 2)
|
||||||
|
|
||||||
|
def test_douban_process_search_results_preserves_special_season_zero(self):
|
||||||
|
"""豆瓣搜索匹配应把 season=0 当作明确季号,而不是默认回第 1 季。"""
|
||||||
|
result = {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type_name": MediaType.TV.value,
|
||||||
|
"target": {
|
||||||
|
"id": "200",
|
||||||
|
"title": "测试剧 S01",
|
||||||
|
"type": "tv",
|
||||||
|
"year": "2024",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type_name": MediaType.TV.value,
|
||||||
|
"target": {
|
||||||
|
"id": "201",
|
||||||
|
"title": "测试剧 S00",
|
||||||
|
"type": "tv",
|
||||||
|
"year": "2024",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
matched = DoubanModule._process_search_results(
|
||||||
|
result,
|
||||||
|
"测试剧",
|
||||||
|
mtype=MediaType.TV,
|
||||||
|
year="2024",
|
||||||
|
season=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(matched["id"], "201")
|
||||||
|
|||||||
@@ -146,6 +146,19 @@ class MetaInfoTest(TestCase):
|
|||||||
self.assertEqual(meta.episode_group, group_id)
|
self.assertEqual(meta.episode_group, group_id)
|
||||||
self.assertEqual(meta.apply_words, custom_words)
|
self.assertEqual(meta.apply_words, custom_words)
|
||||||
|
|
||||||
|
def test_custom_words_support_special_season_zero_parameter(self):
|
||||||
|
"""显式媒体标签中的 s=0 应作为特别季写入元数据。"""
|
||||||
|
custom_words = [
|
||||||
|
"Test Show => 测试剧 {[tmdbid=12345;type=tv;s=0]}"
|
||||||
|
]
|
||||||
|
|
||||||
|
with patch("app.core.metainfo.rust_accel.parse_metainfo", return_value=None):
|
||||||
|
meta = MetaInfo(title="Test Show 01", custom_words=custom_words)
|
||||||
|
|
||||||
|
self.assertEqual(meta.tmdbid, 12345)
|
||||||
|
self.assertEqual(meta.type.value, "电视剧")
|
||||||
|
self.assertEqual(meta.begin_season, 0)
|
||||||
|
|
||||||
def test_find_metainfo_supports_episode_group_parameter(self):
|
def test_find_metainfo_supports_episode_group_parameter(self):
|
||||||
"""测试显式媒体标签支持 g 剧集组参数"""
|
"""测试显式媒体标签支持 g 剧集组参数"""
|
||||||
group_id = "5ad0ec240e0a26303f00d84d"
|
group_id = "5ad0ec240e0a26303f00d84d"
|
||||||
|
|||||||
@@ -121,7 +121,25 @@ def _load_subscribe_chain_class():
|
|||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
|
|
||||||
class _SubscribeSchema:
|
class _SubscribeSchema:
|
||||||
|
_fields = {
|
||||||
|
"name",
|
||||||
|
"type",
|
||||||
|
"year",
|
||||||
|
"tmdbid",
|
||||||
|
"doubanid",
|
||||||
|
"bangumiid",
|
||||||
|
"season",
|
||||||
|
"best_version",
|
||||||
|
"save_path",
|
||||||
|
"search_imdbid",
|
||||||
|
"custom_words",
|
||||||
|
"media_category",
|
||||||
|
"filter_groups",
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
for field in self._fields:
|
||||||
|
setattr(self, field, None)
|
||||||
for key, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
|
|
||||||
@@ -333,6 +351,7 @@ class SubscribeChainTest(TestCase):
|
|||||||
"year": "2026",
|
"year": "2026",
|
||||||
"imdbid": None,
|
"imdbid": None,
|
||||||
"tvdbid": None,
|
"tvdbid": None,
|
||||||
|
"bangumiid": None,
|
||||||
"episode_group": None,
|
"episode_group": None,
|
||||||
"poster": None,
|
"poster": None,
|
||||||
"backdrop": None,
|
"backdrop": None,
|
||||||
@@ -353,6 +372,22 @@ class SubscribeChainTest(TestCase):
|
|||||||
media_info=SimpleNamespace(type=MediaType.TV, tmdb_id=1, douban_id=None),
|
media_info=SimpleNamespace(type=MediaType.TV, tmdb_id=1, douban_id=None),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_format_subscribe_progress_preserves_special_season_zero(self):
|
||||||
|
"""订阅列表展示必须把 S0 当作合法季号,而不是回退到第 1 季。"""
|
||||||
|
subscribe = self._build_subscribe(season=0, total_episode=5, lack_episode=2)
|
||||||
|
|
||||||
|
progress = SubscribeChain._format_subscribe_progress(subscribe)
|
||||||
|
|
||||||
|
self.assertEqual(progress, "第0季 [3/5]")
|
||||||
|
|
||||||
|
def test_format_subscribe_progress_preserves_special_season_zero_without_total(self):
|
||||||
|
"""S0 没有总集数时仍显示特别季季号。"""
|
||||||
|
subscribe = self._build_subscribe(season=0, total_episode=None, lack_episode=None)
|
||||||
|
|
||||||
|
progress = SubscribeChain._format_subscribe_progress(subscribe)
|
||||||
|
|
||||||
|
self.assertEqual(progress, "第0季")
|
||||||
|
|
||||||
def test_match_title_fallback_calls_torrent_match_from_class(self):
|
def test_match_title_fallback_calls_torrent_match_from_class(self):
|
||||||
"""确保标题兜底匹配不依赖 TorrentHelper 实例绑定。"""
|
"""确保标题兜底匹配不依赖 TorrentHelper 实例绑定。"""
|
||||||
|
|
||||||
@@ -420,6 +455,98 @@ class SubscribeChainTest(TestCase):
|
|||||||
), self.assertRaises(_ReachedTitleMatch):
|
), self.assertRaises(_ReachedTitleMatch):
|
||||||
chain.match({"test.example": [context]})
|
chain.match({"test.example": [context]})
|
||||||
|
|
||||||
|
def test_match_accepts_special_season_zero_candidate(self):
|
||||||
|
"""S0 订阅应允许 S00 候选资源进入下载候选,不能按未指定季处理。"""
|
||||||
|
|
||||||
|
class _TorrentHelper:
|
||||||
|
def filter_torrent(self, *args, **kwargs):
|
||||||
|
return True
|
||||||
|
|
||||||
|
subscribe = self._build_subscribe(
|
||||||
|
best_version=0,
|
||||||
|
custom_words=None,
|
||||||
|
doubanid=None,
|
||||||
|
episode_group=None,
|
||||||
|
filter_groups=[],
|
||||||
|
keyword=None,
|
||||||
|
media_category=None,
|
||||||
|
save_path=None,
|
||||||
|
search_imdbid=False,
|
||||||
|
season=0,
|
||||||
|
sites=[],
|
||||||
|
tmdbid=1,
|
||||||
|
username="",
|
||||||
|
downloader=None,
|
||||||
|
)
|
||||||
|
mediainfo = SimpleNamespace(
|
||||||
|
clear=lambda: None,
|
||||||
|
douban_id=None,
|
||||||
|
title_year="Test Show (2026)",
|
||||||
|
tmdb_id=1,
|
||||||
|
type=MediaType.TV,
|
||||||
|
)
|
||||||
|
torrent_media = SimpleNamespace(
|
||||||
|
clear=lambda: None,
|
||||||
|
douban_id=None,
|
||||||
|
tmdb_id=1,
|
||||||
|
type=MediaType.TV,
|
||||||
|
)
|
||||||
|
context = SimpleNamespace(
|
||||||
|
media_info=torrent_media,
|
||||||
|
media_recognize_fail_count=0,
|
||||||
|
meta_info=SimpleNamespace(
|
||||||
|
begin_season=0,
|
||||||
|
episode_list=[1],
|
||||||
|
org_string="Test Show S00E01",
|
||||||
|
season_list=[0],
|
||||||
|
),
|
||||||
|
torrent_info=SimpleNamespace(
|
||||||
|
description="",
|
||||||
|
pri_order=100,
|
||||||
|
site=1,
|
||||||
|
site_name="TestSite",
|
||||||
|
title="Test Show S00E01",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
download_calls = []
|
||||||
|
|
||||||
|
class _SubscribeOper:
|
||||||
|
"""提供单条订阅,避免依赖真实数据库。"""
|
||||||
|
|
||||||
|
def list(self, *args, **kwargs):
|
||||||
|
"""返回当前测试构造的订阅列表。"""
|
||||||
|
return [subscribe]
|
||||||
|
|
||||||
|
def get(self, *args, **kwargs):
|
||||||
|
"""下载后仍返回当前订阅。"""
|
||||||
|
return subscribe
|
||||||
|
|
||||||
|
def _download(self, **kwargs):
|
||||||
|
download_calls.append(kwargs)
|
||||||
|
return [context], {}
|
||||||
|
|
||||||
|
chain = SubscribeChain()
|
||||||
|
chain.recognize_media = lambda **kwargs: mediainfo
|
||||||
|
chain.check_and_handle_existing_media = lambda **kwargs: (False, {})
|
||||||
|
chain.get_sub_sites = lambda *_args, **_kwargs: []
|
||||||
|
chain.get_params = lambda *_args, **_kwargs: {}
|
||||||
|
chain.filter_torrents = lambda **_kwargs: [context.torrent_info]
|
||||||
|
chain.finish_subscribe_or_not = lambda **_kwargs: None
|
||||||
|
|
||||||
|
with patch.object(SUBSCRIBE_CHAIN_MODULE, "SubscribeOper", _SubscribeOper), patch.object(
|
||||||
|
SUBSCRIBE_CHAIN_MODULE,
|
||||||
|
"TorrentHelper",
|
||||||
|
_TorrentHelper,
|
||||||
|
), patch.object(
|
||||||
|
SubscribeChain,
|
||||||
|
"_SubscribeChain__download_best_version_with_full_pack_first",
|
||||||
|
_download,
|
||||||
|
):
|
||||||
|
chain.match({"test.example": [context]})
|
||||||
|
|
||||||
|
self.assertEqual(len(download_calls), 1)
|
||||||
|
self.assertEqual(download_calls[0]["contexts"][0].meta_info.begin_season, 0)
|
||||||
|
|
||||||
def test_get_episode_priority_falls_back_to_current_priority(self):
|
def test_get_episode_priority_falls_back_to_current_priority(self):
|
||||||
subscribe = self._build_subscribe(current_priority=80, episode_priority=None)
|
subscribe = self._build_subscribe(current_priority=80, episode_priority=None)
|
||||||
|
|
||||||
@@ -667,6 +794,132 @@ class SubscribeChainTest(TestCase):
|
|||||||
self.assertEqual(subscribe.lack_episode, 0)
|
self.assertEqual(subscribe.lack_episode, 0)
|
||||||
self.assertEqual(subscribe.note, list(range(1, 11)))
|
self.assertEqual(subscribe.note, list(range(1, 11)))
|
||||||
|
|
||||||
|
def test_resolve_subscribe_missing_preserves_special_season_zero_totals(self):
|
||||||
|
"""特别季 S0 是合法订阅季,目标满足查询必须按订阅总集数裁剪媒体库缺集。"""
|
||||||
|
subscribe = self._build_subscribe(
|
||||||
|
best_version=0,
|
||||||
|
season=0,
|
||||||
|
total_episode=5,
|
||||||
|
lack_episode=2,
|
||||||
|
note=[1, 2, 3],
|
||||||
|
)
|
||||||
|
meta = SimpleNamespace(type=MediaType.TV, begin_season=0, season=0)
|
||||||
|
mediainfo = SimpleNamespace(
|
||||||
|
type=MediaType.TV,
|
||||||
|
seasons={0: list(range(1, 4))},
|
||||||
|
title_year="Test Show (2026)",
|
||||||
|
)
|
||||||
|
captured_totals = []
|
||||||
|
|
||||||
|
class _DownloadChain:
|
||||||
|
def get_no_exists_info(self, **kwargs):
|
||||||
|
captured_totals.append(kwargs["totals"])
|
||||||
|
if kwargs["totals"] == {0: 5}:
|
||||||
|
return False, {
|
||||||
|
1: {
|
||||||
|
0: SimpleNamespace(
|
||||||
|
season=0,
|
||||||
|
episodes=[4, 5],
|
||||||
|
total_episode=5,
|
||||||
|
start_episode=1,
|
||||||
|
require_complete_coverage=False,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return True, {}
|
||||||
|
|
||||||
|
with patch.object(SUBSCRIBE_CHAIN_MODULE, "DownloadChain", _DownloadChain):
|
||||||
|
satisfied, no_exists = SubscribeChain().resolve_subscribe_missing(
|
||||||
|
subscribe=subscribe,
|
||||||
|
meta=meta,
|
||||||
|
mediainfo=mediainfo,
|
||||||
|
mediakey=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(satisfied)
|
||||||
|
self.assertEqual(captured_totals, [{0: 5}])
|
||||||
|
self.assertEqual(no_exists[1][0].episodes, [4, 5])
|
||||||
|
|
||||||
|
def test_build_subscribe_meta_preserves_special_season_zero(self):
|
||||||
|
"""订阅构造 MetaInfo 的统一入口必须保留 S0。"""
|
||||||
|
subscribe = self._build_subscribe(season=0)
|
||||||
|
|
||||||
|
meta = SUBSCRIBE_CHAIN_MODULE.build_subscribe_meta(subscribe)
|
||||||
|
|
||||||
|
self.assertEqual(meta.begin_season, 0)
|
||||||
|
self.assertEqual(meta.type, MediaType.TV)
|
||||||
|
|
||||||
|
def test_follow_preserves_shared_special_season_zero(self):
|
||||||
|
"""follow 分享订阅携带 S0 时,标题规整不能把合法季号覆盖成未指定。"""
|
||||||
|
added_calls = []
|
||||||
|
|
||||||
|
class _SubscribeOper:
|
||||||
|
"""提供订阅存在性查询,避免依赖真实数据库。"""
|
||||||
|
|
||||||
|
def exists(self, *args, **kwargs):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def exist_history(self, *args, **kwargs):
|
||||||
|
return False
|
||||||
|
|
||||||
|
class _SystemConfigOper:
|
||||||
|
"""提供 follow 用户配置。"""
|
||||||
|
|
||||||
|
def get(self, *args, **kwargs):
|
||||||
|
return ["follow-user"]
|
||||||
|
|
||||||
|
class _MoviePilotServerHelper:
|
||||||
|
"""提供单条 S0 分享订阅。"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_subscribe_shares():
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"share_uid": "follow-user",
|
||||||
|
"name": "Test Show",
|
||||||
|
"type": MediaType.TV.value,
|
||||||
|
"year": "2026",
|
||||||
|
"tmdbid": None,
|
||||||
|
"doubanid": "12345",
|
||||||
|
"season": 0,
|
||||||
|
"best_version": 0,
|
||||||
|
"save_path": None,
|
||||||
|
"search_imdbid": False,
|
||||||
|
"custom_words": None,
|
||||||
|
"media_category": None,
|
||||||
|
"filter_groups": [],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def _add(self, **kwargs):
|
||||||
|
added_calls.append(kwargs)
|
||||||
|
return 1, None
|
||||||
|
|
||||||
|
def _metainfo(title):
|
||||||
|
return SimpleNamespace(name=title, begin_season=None, episode_list=[])
|
||||||
|
|
||||||
|
with patch.object(SUBSCRIBE_CHAIN_MODULE, "SubscribeOper", _SubscribeOper), patch.object(
|
||||||
|
SUBSCRIBE_CHAIN_MODULE,
|
||||||
|
"SystemConfigOper",
|
||||||
|
_SystemConfigOper,
|
||||||
|
), patch.object(
|
||||||
|
SUBSCRIBE_CHAIN_MODULE,
|
||||||
|
"MoviePilotServerHelper",
|
||||||
|
_MoviePilotServerHelper,
|
||||||
|
), patch.object(
|
||||||
|
SUBSCRIBE_CHAIN_MODULE,
|
||||||
|
"MetaInfo",
|
||||||
|
_metainfo,
|
||||||
|
), patch.object(
|
||||||
|
SubscribeChain,
|
||||||
|
"add",
|
||||||
|
_add,
|
||||||
|
):
|
||||||
|
SubscribeChain.follow()
|
||||||
|
|
||||||
|
self.assertEqual(len(added_calls), 1)
|
||||||
|
self.assertEqual(added_calls[0]["season"], 0)
|
||||||
|
|
||||||
def test_resolve_subscribe_missing_accepts_downloaded_episode_best_version_targets(self):
|
def test_resolve_subscribe_missing_accepts_downloaded_episode_best_version_targets(self):
|
||||||
"""外部完成守卫可按任意已下载版本判定分集洗版目标已满足。"""
|
"""外部完成守卫可按任意已下载版本判定分集洗版目标已满足。"""
|
||||||
subscribe = self._build_subscribe(
|
subscribe = self._build_subscribe(
|
||||||
|
|||||||
@@ -42,3 +42,34 @@ class SubscribeEndpointTest(TestCase):
|
|||||||
self.assertTrue(response.success)
|
self.assertTrue(response.success)
|
||||||
self.assertNotIn("completed_episode", async_add.await_args.kwargs)
|
self.assertNotIn("completed_episode", async_add.await_args.kwargs)
|
||||||
self.assertEqual(async_add.await_args.kwargs["username"], "moviepilot-user")
|
self.assertEqual(async_add.await_args.kwargs["username"], "moviepilot-user")
|
||||||
|
|
||||||
|
def test_create_subscribe_preserves_special_season_zero_with_doubanid(self):
|
||||||
|
"""
|
||||||
|
新增订阅带豆瓣 ID 且显式指定 S0 时,标题规整不应覆盖调用方传入的季号。
|
||||||
|
"""
|
||||||
|
subscribe_in = Subscribe(
|
||||||
|
name="测试剧集",
|
||||||
|
year="2026",
|
||||||
|
type=MediaType.TV.value,
|
||||||
|
doubanid="12345",
|
||||||
|
season=0,
|
||||||
|
total_episode=5,
|
||||||
|
lack_episode=5,
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"app.api.endpoints.subscribe.MetaInfo",
|
||||||
|
return_value=SimpleNamespace(name="测试剧集", begin_season=None),
|
||||||
|
), patch(
|
||||||
|
"app.api.endpoints.subscribe.SubscribeChain.async_add",
|
||||||
|
new=AsyncMock(return_value=(1, "新增订阅成功")),
|
||||||
|
) as async_add:
|
||||||
|
response = asyncio.run(
|
||||||
|
create_subscribe(
|
||||||
|
subscribe_in=subscribe_in,
|
||||||
|
current_user=SimpleNamespace(name="moviepilot-user"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(response.success)
|
||||||
|
self.assertEqual(async_add.await_args.kwargs["season"], 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user