mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-01 07:26:50 +00:00
feat(subscribe): 新增订阅总集数刷新与完成检查链式事件 (#5860)
This commit is contained in:
@@ -37,7 +37,8 @@ from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.helper.server import MoviePilotServerHelper
|
||||
from app.helper.torrent import TorrentHelper
|
||||
from app.log import logger
|
||||
from app.schemas import MediaRecognizeConvertEventData
|
||||
from app.schemas import (MediaRecognizeConvertEventData, SubscribeEpisodesRefreshEventData,
|
||||
SubscribeCompletionCheckEventData)
|
||||
from app.schemas.types import MediaType, SystemConfigKey, MessageChannel, NotificationType, EventType, ChainEventType, \
|
||||
ContentType
|
||||
|
||||
@@ -640,6 +641,10 @@ class SubscribeChain(ChainBase):
|
||||
logger.error(f"媒体信息中没有季集信息,标题:{title},tmdbid:{tmdbid},doubanid:{doubanid}")
|
||||
return None, "媒体信息中没有季集信息"
|
||||
total_episode = len(mediainfo.seasons.get(season) or [])
|
||||
# 允许外部覆盖按 TMDB 算出的总集数(如待定集数)
|
||||
total_episode = self.__apply_episodes_refresh(
|
||||
total_episode, season=season, mediainfo=mediainfo,
|
||||
tmdbid=mediainfo.tmdb_id, doubanid=mediainfo.douban_id, scene="create")
|
||||
if not total_episode:
|
||||
logger.error(f'未获取到总集数,标题:{title},tmdbid:{tmdbid}, doubanid:{doubanid}')
|
||||
return None, f"未获取到第 {season} 季的总集数"
|
||||
@@ -821,6 +826,10 @@ class SubscribeChain(ChainBase):
|
||||
logger.error(f"媒体信息中没有季集信息,标题:{title},tmdbid:{tmdbid},doubanid:{doubanid}")
|
||||
return None, "媒体信息中没有季集信息"
|
||||
total_episode = len(mediainfo.seasons.get(season) or [])
|
||||
# 允许外部覆盖按 TMDB 算出的总集数(如待定集数)
|
||||
total_episode = await self.__async_apply_episodes_refresh(
|
||||
total_episode, season=season, mediainfo=mediainfo,
|
||||
tmdbid=mediainfo.tmdb_id, doubanid=mediainfo.douban_id, scene="create")
|
||||
if not total_episode:
|
||||
logger.error(f'未获取到总集数,标题:{title},tmdbid:{tmdbid}, doubanid:{doubanid}')
|
||||
return None, f"未获取到第 {season} 季的总集数"
|
||||
@@ -1687,8 +1696,12 @@ class SubscribeChain(ChainBase):
|
||||
current_priority = None
|
||||
if not subscribe.manual_total_episode and len(episodes):
|
||||
total_episode = len(episodes)
|
||||
# 总集数增长按 delta 同步抬升 lack
|
||||
lack_episode = (subscribe.lack_episode or 0) + (total_episode - (subscribe.total_episode or 0))
|
||||
# 允许外部覆盖按 TMDB 算出的总集数(如待定集数)
|
||||
total_episode = self.__apply_episodes_refresh(
|
||||
total_episode, season=subscribe.season, mediainfo=mediainfo,
|
||||
tmdbid=subscribe.tmdbid, doubanid=subscribe.doubanid,
|
||||
subscribe_id=subscribe.id, scene="refresh")
|
||||
lack_episode = max((subscribe.lack_episode or 0) + (total_episode - (subscribe.total_episode or 0)), 0)
|
||||
if subscribe.best_version and subscribe.type == MediaType.TV.value:
|
||||
# 为新增集补齐 episode_priority 初始项(priority=0)
|
||||
old_total_episode = subscribe.total_episode or 0
|
||||
@@ -1990,6 +2003,15 @@ class SubscribeChain(ChainBase):
|
||||
# 如果订阅状态为待定(P),说明订阅信息尚未完全更新,无法完成订阅
|
||||
if subscribe.state == "P":
|
||||
return
|
||||
# 发送订阅完成判定事件,在写入 DB 前,允许外部据完结策略否决本次自动完成
|
||||
completion_event = eventmanager.send_event(
|
||||
ChainEventType.SubscribeCompletionCheck,
|
||||
SubscribeCompletionCheckEventData(subscribe=subscribe, mediainfo=mediainfo, meta=meta))
|
||||
if completion_event and completion_event.event_data:
|
||||
completion_data: SubscribeCompletionCheckEventData = completion_event.event_data
|
||||
if completion_data.cancel:
|
||||
logger.info(f'{mediainfo.title_year} 完成被 [{completion_data.source}] 否决:{completion_data.reason}')
|
||||
return
|
||||
# 完成订阅
|
||||
msgstr = "订阅" if not subscribe.best_version else "洗版"
|
||||
logger.info(f'{mediainfo.title_year} 完成{msgstr}')
|
||||
@@ -3124,7 +3146,52 @@ class SubscribeChain(ChainBase):
|
||||
return False, no_exists
|
||||
|
||||
@staticmethod
|
||||
def __refresh_total_episode_before_completion(subscribe: Subscribe, mediainfo: MediaInfo):
|
||||
def __apply_episodes_refresh(current_total: int, season: Optional[int], *,
|
||||
mediainfo: Optional[MediaInfo] = None,
|
||||
tmdbid: Optional[int] = None,
|
||||
doubanid: Optional[str] = None,
|
||||
subscribe_id: Optional[int] = None,
|
||||
scene: Optional[str] = None) -> int:
|
||||
"""
|
||||
发送订阅总集数推算事件,允许外部据自身策略覆盖按 TMDB 季集数算出的总集数。
|
||||
|
||||
用途:插件在"待定集数"等场景经事件注入 total_episode
|
||||
无监听者或外部未覆盖时返回入参原值,保证零行为变更。
|
||||
:param current_total: 主程序按 TMDB 季集数算出的默认总集数
|
||||
:param season: 季号
|
||||
:return: 最终采用的总集数
|
||||
"""
|
||||
event_data = SubscribeEpisodesRefreshEventData(
|
||||
tmdbid=tmdbid, doubanid=doubanid, season=season, mediainfo=mediainfo,
|
||||
current_total_episode=current_total, subscribe_id=subscribe_id, scene=scene)
|
||||
event = eventmanager.send_event(ChainEventType.SubscribeEpisodesRefresh, event_data)
|
||||
if event and event.event_data:
|
||||
result: SubscribeEpisodesRefreshEventData = event.event_data
|
||||
if result.updated and result.total_episode:
|
||||
return result.total_episode
|
||||
return current_total
|
||||
|
||||
@staticmethod
|
||||
async def __async_apply_episodes_refresh(current_total: int, season: Optional[int], *,
|
||||
mediainfo: Optional[MediaInfo] = None,
|
||||
tmdbid: Optional[int] = None,
|
||||
doubanid: Optional[str] = None,
|
||||
subscribe_id: Optional[int] = None,
|
||||
scene: Optional[str] = None) -> int:
|
||||
"""
|
||||
__apply_episodes_refresh 的异步版本
|
||||
"""
|
||||
event_data = SubscribeEpisodesRefreshEventData(
|
||||
tmdbid=tmdbid, doubanid=doubanid, season=season, mediainfo=mediainfo,
|
||||
current_total_episode=current_total, subscribe_id=subscribe_id, scene=scene)
|
||||
event = await eventmanager.async_send_event(ChainEventType.SubscribeEpisodesRefresh, event_data)
|
||||
if event and event.event_data:
|
||||
result: SubscribeEpisodesRefreshEventData = event.event_data
|
||||
if result.updated and result.total_episode:
|
||||
return result.total_episode
|
||||
return current_total
|
||||
|
||||
def __refresh_total_episode_before_completion(self, subscribe: Subscribe, mediainfo: MediaInfo):
|
||||
"""
|
||||
在完成判断前,按最新识别结果兜底修正订阅总集数,防止旧总集数导致误完成。
|
||||
"""
|
||||
@@ -3136,6 +3203,11 @@ class SubscribeChain(ChainBase):
|
||||
return
|
||||
|
||||
new_total_episode = len((mediainfo.seasons or {}).get(subscribe.season) or [])
|
||||
# 允许外部覆盖按 TMDB 算出的总集数(如待定集数),后续“只增不减”仍作用于覆盖后的结果,避免误减导致提前完成。
|
||||
new_total_episode = self.__apply_episodes_refresh(
|
||||
new_total_episode, season=subscribe.season, mediainfo=mediainfo,
|
||||
tmdbid=subscribe.tmdbid, doubanid=subscribe.doubanid,
|
||||
subscribe_id=subscribe.id, scene="precheck")
|
||||
old_total_episode = subscribe.total_episode or 0
|
||||
if not new_total_episode or new_total_episode <= old_total_episode:
|
||||
return
|
||||
|
||||
@@ -549,3 +549,71 @@ class StorageOperSelectionEventData(ChainEventData):
|
||||
|
||||
# 输出参数
|
||||
storage_oper: Optional[Callable] = Field(default=None, description="存储操作对象")
|
||||
|
||||
|
||||
class SubscribeEpisodesRefreshEventData(ChainEventData):
|
||||
"""
|
||||
SubscribeEpisodesRefresh 事件的数据模型
|
||||
|
||||
主程序在推算订阅某季总集数时发出,携带按 TMDB 季集数算出的默认值,外部可据自身策略覆盖total_episode(如待定集数)
|
||||
|
||||
Attributes:
|
||||
# 输入参数
|
||||
tmdbid (Optional[int]): TMDB ID
|
||||
doubanid (Optional[str]): 豆瓣 ID
|
||||
season (Optional[int]): 季号
|
||||
mediainfo (Any): 媒体信息
|
||||
current_total_episode (int): 主程序按 TMDB 季集数算出的默认总集数
|
||||
subscribe_id (Optional[int]): 订阅 ID;订阅创建场景下尚未入库,为空
|
||||
scene (Optional[str]): 触发场景,create/refresh/precheck
|
||||
|
||||
# 输出参数
|
||||
updated (bool): 外部是否覆盖了总集数,默认 False
|
||||
total_episode (Optional[int]): 覆盖后的总集数,仅在 updated=True 时生效
|
||||
source (str): 覆盖来源
|
||||
reason (str): 覆盖原因
|
||||
"""
|
||||
|
||||
# 输入参数
|
||||
tmdbid: Optional[int] = Field(default=None, description="TMDB ID")
|
||||
doubanid: Optional[str] = Field(default=None, description="豆瓣 ID")
|
||||
season: Optional[int] = Field(default=None, description="季号")
|
||||
mediainfo: Any = Field(default=None, description="媒体信息")
|
||||
current_total_episode: int = Field(default=0, description="按 TMDB 季集数算出的默认总集数")
|
||||
subscribe_id: Optional[int] = Field(default=None, description="订阅 ID;创建场景为空")
|
||||
scene: Optional[str] = Field(default=None, description="触发场景:create/refresh/precheck")
|
||||
|
||||
# 输出参数
|
||||
updated: bool = Field(default=False, description="外部是否覆盖了总集数")
|
||||
total_episode: Optional[int] = Field(default=None, description="覆盖后的总集数")
|
||||
source: str = Field(default="未知来源", description="覆盖来源")
|
||||
reason: str = Field(default="", description="覆盖原因")
|
||||
|
||||
|
||||
class SubscribeCompletionCheckEventData(ChainEventData):
|
||||
"""
|
||||
SubscribeCompletionCheck 事件的数据模型
|
||||
|
||||
在订阅被自动判定完成、即将收口(写历史并删除)之前发出,允许外部据完结策略否决本次完成
|
||||
|
||||
Attributes:
|
||||
# 输入参数
|
||||
subscribe (Any): 订阅对象
|
||||
mediainfo (Any): 媒体信息
|
||||
meta (Any): 元数据
|
||||
|
||||
# 输出参数
|
||||
cancel (bool): 是否否决本次完成,默认 False
|
||||
source (str): 否决来源
|
||||
reason (str): 否决原因
|
||||
"""
|
||||
|
||||
# 输入参数
|
||||
subscribe: Any = Field(default=None, description="订阅对象")
|
||||
mediainfo: Any = Field(default=None, description="媒体信息")
|
||||
meta: Any = Field(default=None, description="元数据")
|
||||
|
||||
# 输出参数
|
||||
cancel: bool = Field(default=False, description="是否否决本次完成")
|
||||
source: str = Field(default="未知来源", description="否决来源")
|
||||
reason: str = Field(default="", description="否决原因")
|
||||
|
||||
@@ -179,6 +179,10 @@ class ChainEventType(Enum):
|
||||
StorageOperSelection = "storage.operation"
|
||||
# Agent LLM 供应商选择
|
||||
AgentLLMProvider = "agent.llm.provider"
|
||||
# 订阅总集数刷新
|
||||
SubscribeEpisodesRefresh = "subscribe.episodes.refresh"
|
||||
# 订阅完成检查
|
||||
SubscribeCompletionCheck = "subscribe.completion.check"
|
||||
|
||||
|
||||
# 系统配置Key字典
|
||||
|
||||
Reference in New Issue
Block a user