mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-01 12:56:34 +08:00
fix(workflow): 工作流添加下载支持 MetaInfo.season_list (#6637)
This commit is contained in:
@@ -770,4 +770,5 @@ class MetaBase(object):
|
||||
dicts["edition"] = self.edition
|
||||
dicts["name"] = self.name
|
||||
dicts["episode_list"] = self.episode_list
|
||||
dicts["season_list"] = self.season_list
|
||||
return dicts
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from typing import Annotated, Any, Dict, List, Optional, Union
|
||||
|
||||
from pydantic import BaseModel, Discriminator, Field, RootModel, Tag, model_validator
|
||||
from pydantic import BaseModel, Discriminator, Field, PrivateAttr, RootModel, Tag, model_validator
|
||||
|
||||
from app.schemas.category import ClassificationFactValue, ClassificationResult
|
||||
from app.schemas.common import JsonData
|
||||
from app.schemas.media import OptionalMediaIdentityMixin
|
||||
from app.schemas.music import MusicArtistInfo as _MusicArtistInfo
|
||||
from app.schemas.music import MusicInfo, MusicMeta
|
||||
from app.schemas.types import MediaSource
|
||||
from app.schemas.types import MediaSource, MediaType
|
||||
|
||||
|
||||
class MetaInfo(OptionalMediaIdentityMixin, BaseModel):
|
||||
@@ -75,6 +75,71 @@ class MetaInfo(OptionalMediaIdentityMixin, BaseModel):
|
||||
# 显式媒体数据源原生ID
|
||||
media_id: Optional[str] = None
|
||||
|
||||
_season_list: Optional[List[int]] = PrivateAttr(default=None)
|
||||
|
||||
@model_validator(mode="wrap")
|
||||
@classmethod
|
||||
def _handle_season_list(cls, data: Any, handler: Any) -> "MetaInfo":
|
||||
season_list = None
|
||||
if isinstance(data, dict) and "season_list" in data:
|
||||
data = data.copy()
|
||||
season_list = data.pop("season_list")
|
||||
instance = handler(data)
|
||||
if season_list is not None and isinstance(instance, MetaInfo):
|
||||
instance._season_list = list(season_list)
|
||||
return instance
|
||||
|
||||
@property
|
||||
def season_list(self) -> List[int]:
|
||||
"""返回识别的季数字列表。"""
|
||||
if self._season_list is not None:
|
||||
return self._season_list
|
||||
if self.begin_season is None:
|
||||
if self.type in (MediaType.TV, MediaType.TV.value):
|
||||
return [1]
|
||||
return []
|
||||
if self.end_season is not None:
|
||||
return list(range(self.begin_season, self.end_season + 1))
|
||||
return [self.begin_season]
|
||||
|
||||
@season_list.setter
|
||||
def season_list(self, value: Optional[List[int]]) -> None:
|
||||
self._season_list = list(value) if value is not None else None
|
||||
|
||||
@property
|
||||
def season(self) -> str:
|
||||
"""返回开始季、结束季字符串,确定是剧集没有季的返回S01。"""
|
||||
if self.begin_season is not None:
|
||||
return "S%s" % str(self.begin_season).rjust(2, "0") \
|
||||
if self.end_season is None \
|
||||
else "S%s-S%s" % \
|
||||
(str(self.begin_season).rjust(2, "0"),
|
||||
str(self.end_season).rjust(2, "0"))
|
||||
if self.type in (MediaType.TV, MediaType.TV.value):
|
||||
return "S01"
|
||||
return ""
|
||||
|
||||
@property
|
||||
def season_seq(self) -> str:
|
||||
"""返回 begin_season 的数字,电视剧没有季的返回1。"""
|
||||
if self.begin_season is not None:
|
||||
return str(self.begin_season)
|
||||
if self.type in (MediaType.TV, MediaType.TV.value):
|
||||
return "1"
|
||||
return ""
|
||||
|
||||
@property
|
||||
def episode(self) -> str:
|
||||
"""返回开始集、结束集字符串。"""
|
||||
if self.begin_episode is not None:
|
||||
return "E%s" % str(self.begin_episode).rjust(2, "0") \
|
||||
if self.end_episode is None \
|
||||
else "E%s-E%s" % \
|
||||
(
|
||||
str(self.begin_episode).rjust(2, "0"),
|
||||
str(self.end_episode).rjust(2, "0"))
|
||||
return ""
|
||||
|
||||
|
||||
class MediaImageSet(BaseModel):
|
||||
"""跨媒体源兼容的人物图片尺寸集合。"""
|
||||
|
||||
@@ -86,11 +86,16 @@ class AddDownloadAction(BaseAction):
|
||||
logger.warning(f"{t.meta_info.title} 有多季,跳过")
|
||||
continue
|
||||
else:
|
||||
exists_episodes = exists_seasons.get(t.meta_info.begin_season)
|
||||
if exists_episodes:
|
||||
season_num = (
|
||||
t.meta_info.begin_season
|
||||
if t.meta_info.begin_season is not None
|
||||
else (t.meta_info.season_list[0] if t.meta_info.season_list else 1)
|
||||
)
|
||||
exists_episodes = exists_seasons.get(season_num)
|
||||
if exists_episodes and t.meta_info.episode_list:
|
||||
if set(t.meta_info.episode_list).issubset(exists_episodes):
|
||||
logger.warning(
|
||||
f"{t.meta_info.title} 第 {t.meta_info.begin_season} 季第 {t.meta_info.episode_list} 集已存在,跳过")
|
||||
f"{t.meta_info.title} 第 {season_num} 季第 {t.meta_info.episode_list} 集已存在,跳过")
|
||||
continue
|
||||
|
||||
_started = True
|
||||
|
||||
@@ -46,3 +46,45 @@ def test_last_search_context_keeps_subtitle_result_shape() -> None:
|
||||
assert result["title"] == "Example subtitle"
|
||||
assert result["site_name"] == "Subtitle Site"
|
||||
assert result["enclosure"] == "https://example.test/subtitle.srt"
|
||||
|
||||
|
||||
def test_schema_metainfo_season_and_episode_properties() -> None:
|
||||
"""Schema MetaInfo 应具备与领域 MetaBase 一致的季、集属性和序列化兼容性。"""
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
# 单季剧集(未显式指定季)
|
||||
tv_no_season = MetaInfo(type="电视剧", begin_episode=1, end_episode=2)
|
||||
assert tv_no_season.season_list == [1]
|
||||
assert tv_no_season.season == "S01"
|
||||
assert tv_no_season.season_seq == "1"
|
||||
assert tv_no_season.episode == "E01-E02"
|
||||
|
||||
# 指定单季剧集
|
||||
tv_single_season = MetaInfo(type=MediaType.TV, begin_season=2, begin_episode=5)
|
||||
assert tv_single_season.season_list == [2]
|
||||
assert tv_single_season.season == "S02"
|
||||
assert tv_single_season.season_seq == "2"
|
||||
assert tv_single_season.episode == "E05"
|
||||
|
||||
# 多季剧集
|
||||
tv_multi_season = MetaInfo(type="电视剧", begin_season=1, end_season=3)
|
||||
assert tv_multi_season.season_list == [1, 2, 3]
|
||||
assert tv_multi_season.season == "S01-S03"
|
||||
|
||||
# 电影
|
||||
movie = MetaInfo(type="电影")
|
||||
assert movie.season_list == []
|
||||
assert movie.season == ""
|
||||
assert movie.season_seq == ""
|
||||
assert movie.episode == ""
|
||||
|
||||
# 显式覆盖与字典解析
|
||||
custom = MetaInfo(season_list=[0])
|
||||
assert custom.season_list == [0]
|
||||
|
||||
from_dict = MetaInfo.model_validate({"season_list": [2, 4], "type": "电视剧"})
|
||||
assert from_dict.season_list == [2, 4]
|
||||
|
||||
# setter 覆写
|
||||
tv_no_season.season_list = [5]
|
||||
assert tv_no_season.season_list == [5]
|
||||
|
||||
@@ -249,3 +249,73 @@ def test_workflow_manager_list_actions_exposes_contract():
|
||||
assert isinstance(actions[0]["data"], dict)
|
||||
assert actions[0]["contract"]["outputs"][0]["name"] == "torrents"
|
||||
assert actions[0]["contract"]["condition_fields"][0]["label"] == "资源"
|
||||
|
||||
|
||||
def test_add_download_only_lack_handles_schema_metainfo(monkeypatch):
|
||||
"""添加下载动作开启仅下载缺失资源时应支持 Schema MetaInfo 的 season_list 属性并正确过滤已存在剧集。"""
|
||||
from app.schemas.context import (
|
||||
Context as SchemaContext,
|
||||
MediaInfo as SchemaMediaInfo,
|
||||
MetaInfo as SchemaMetaInfo,
|
||||
TorrentInfo as SchemaTorrentInfo,
|
||||
)
|
||||
from app.schemas.types import MediaType
|
||||
from app.workflow.actions.add_download import AddDownloadAction
|
||||
import app.workflow.actions.add_download as add_download_module
|
||||
|
||||
downloaded = []
|
||||
|
||||
class FakeDownloadChain:
|
||||
def media_exists(self, mediainfo):
|
||||
# 模拟媒体库已存在第 1 季的第 1、2 集
|
||||
return SimpleNamespace(seasons={1: [1, 2]})
|
||||
|
||||
def download_single(self, context=None, **kwargs):
|
||||
downloaded.append(context.torrent_info.title)
|
||||
return f"hash-{context.torrent_info.title}"
|
||||
|
||||
monkeypatch.setattr(add_download_module, "DownloadChain", FakeDownloadChain)
|
||||
monkeypatch.setattr(add_download_module.runtime_stop_state, "is_workflow_stopped", lambda _wid: False)
|
||||
|
||||
action = AddDownloadAction("test-add-download")
|
||||
action.check_cache = lambda _wid, _key: False
|
||||
action.save_cache = lambda _wid, _key: None
|
||||
action.job_done = lambda *_args, **_kwargs: None
|
||||
|
||||
# 1. 第 1 季第 1 集(已存在,跳过)
|
||||
t1 = SchemaContext(
|
||||
meta_info=SchemaMetaInfo(title="Show S01E01", type="电视剧", begin_season=1, episode_list=[1]),
|
||||
media_info=SchemaMediaInfo(title="Show", type=MediaType.TV),
|
||||
torrent_info=SchemaTorrentInfo(title="Show S01E01", site=1),
|
||||
)
|
||||
# 2. 第 1 季第 3 集(缺失,应下载)
|
||||
t2 = SchemaContext(
|
||||
meta_info=SchemaMetaInfo(title="Show S01E03", type="电视剧", begin_season=1, episode_list=[3]),
|
||||
media_info=SchemaMediaInfo(title="Show", type=MediaType.TV),
|
||||
torrent_info=SchemaTorrentInfo(title="Show S01E03", site=1),
|
||||
)
|
||||
# 3. 多季资源(有多季,跳过)
|
||||
t3 = SchemaContext(
|
||||
meta_info=SchemaMetaInfo(title="Show S01-S02", type="电视剧", begin_season=1, end_season=2, episode_list=[1]),
|
||||
media_info=SchemaMediaInfo(title="Show", type=MediaType.TV),
|
||||
torrent_info=SchemaTorrentInfo(title="Show S01-S02", site=1),
|
||||
)
|
||||
# 4. 未显式标季但为电视剧(begin_season=None,默认归入第 1 季;第 1 集已存在,跳过)
|
||||
t4 = SchemaContext(
|
||||
meta_info=SchemaMetaInfo(title="Show Ep01", type="电视剧", begin_season=None, episode_list=[1]),
|
||||
media_info=SchemaMediaInfo(title="Show", type=MediaType.TV),
|
||||
torrent_info=SchemaTorrentInfo(title="Show Ep01", site=1),
|
||||
)
|
||||
# 5. 未显式标季但为电视剧(begin_season=None,默认归入第 1 季;第 4 集缺失,应下载)
|
||||
t5 = SchemaContext(
|
||||
meta_info=SchemaMetaInfo(title="Show Ep04", type="电视剧", begin_season=None, episode_list=[4]),
|
||||
media_info=SchemaMediaInfo(title="Show", type=MediaType.TV),
|
||||
torrent_info=SchemaTorrentInfo(title="Show Ep04", site=1),
|
||||
)
|
||||
|
||||
context = ActionContext(torrents=[t1, t2, t3, t4, t5])
|
||||
result = action.execute(workflow_id=1, params={"only_lack": True}, context=context)
|
||||
|
||||
assert downloaded == ["Show S01E03", "Show Ep04"]
|
||||
assert len(result.downloads) == 2
|
||||
assert [d.download_id for d in result.downloads] == ["hash-Show S01E03", "hash-Show Ep04"]
|
||||
|
||||
Reference in New Issue
Block a user