mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 07:27:15 +08:00
feat(filemanager): add TransferRenameBuild chain event and fix TemplateContextBuilder concurrency (#5792)
This commit is contained in:
+79
-48
@@ -28,11 +28,16 @@ from app.utils.string import StringUtils
|
||||
|
||||
class TemplateContextBuilder:
|
||||
"""
|
||||
模板上下文构建器
|
||||
"""
|
||||
模板上下文构建器。
|
||||
|
||||
def __init__(self):
|
||||
self._context = {}
|
||||
无状态实现:所有 ``_add_*`` 方法均为静态方法,接受并就地修改调用方提供的
|
||||
``context`` 字典。``build`` 每次调用都基于一份新的本地字典装填后返回,
|
||||
实例自身不持有任何中间状态——可以被多线程共享调用而不会产生互相串味的
|
||||
``rename_dict``,配合 ``settings.TRANSFER_THREADS > 1`` 的并发整理场景安全。
|
||||
|
||||
保留为类(而非自由函数)是为了向后兼容现有调用方式
|
||||
(``TemplateHelper().builder.build(...)``)。
|
||||
"""
|
||||
|
||||
def build(
|
||||
self,
|
||||
@@ -46,55 +51,64 @@ class TemplateContextBuilder:
|
||||
**kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
:param meta: 媒体信息
|
||||
:param mediainfo: 媒体信息
|
||||
构建一次性渲染上下文字典。
|
||||
|
||||
每次调用都新建本地 ``context`` 字典,依次填充各业务来源后返回过滤掉
|
||||
None 值的副本,调用之间互不影响。
|
||||
|
||||
:param meta: 媒体元数据
|
||||
:param mediainfo: 识别的媒体信息
|
||||
:param torrentinfo: 种子信息
|
||||
:param transferinfo: 传输信息
|
||||
:param transferinfo: 整理结果信息
|
||||
:param file_extension: 文件扩展名
|
||||
:param episodes_info: 剧集信息
|
||||
:param include_raw_objects: 是否包含原始对象
|
||||
:param episodes_info: 当前季的全部集信息
|
||||
:param include_raw_objects: 是否在 dict 里附带原始对象引用(``__meta__`` 等)
|
||||
:return: 渲染上下文字典
|
||||
"""
|
||||
self._context.clear()
|
||||
self._add_episode_details(meta, episodes_info)
|
||||
self._add_media_info(mediainfo)
|
||||
self._add_transfer_info(transferinfo)
|
||||
self._add_torrent_info(torrentinfo)
|
||||
self._add_file_info(file_extension)
|
||||
context: Dict[str, Any] = {}
|
||||
self._add_episode_details(context, meta, episodes_info)
|
||||
self._add_media_info(context, mediainfo)
|
||||
self._add_transfer_info(context, transferinfo)
|
||||
self._add_torrent_info(context, torrentinfo)
|
||||
self._add_file_info(context, file_extension)
|
||||
if kwargs:
|
||||
self._context.update(kwargs)
|
||||
context.update(kwargs)
|
||||
|
||||
if include_raw_objects:
|
||||
self._add_raw_objects(meta, mediainfo, torrentinfo, transferinfo, episodes_info)
|
||||
self._add_raw_objects(context, meta, mediainfo, torrentinfo, transferinfo, episodes_info)
|
||||
|
||||
# 移除空值
|
||||
return {k: v for k, v in self._context.items() if v is not None}
|
||||
return {k: v for k, v in context.items() if v is not None}
|
||||
|
||||
def _add_media_info(self, mediainfo: MediaInfo):
|
||||
@classmethod
|
||||
def _add_media_info(cls, context: Dict[str, Any], mediainfo: Optional[MediaInfo]) -> None:
|
||||
"""
|
||||
增加媒体信息
|
||||
将 MediaInfo 中的标题、季年份、海报等业务字段就地写入 ``context``。
|
||||
|
||||
会读取 ``context`` 中由 ``_add_episode_details`` 先填好的 ``season`` /
|
||||
``year`` / ``title_year`` 占位,保证电视剧场景下季/年优先沿用 meta 解析值。
|
||||
"""
|
||||
if not mediainfo:
|
||||
return
|
||||
season_fmt = f"S{mediainfo.season:02d}" if mediainfo.season is not None else None
|
||||
base_info = {
|
||||
# 标题
|
||||
"title": self.__convert_invalid_characters(mediainfo.title),
|
||||
"title": cls.__convert_invalid_characters(mediainfo.title),
|
||||
# 英文标题
|
||||
"en_title": self.__convert_invalid_characters(mediainfo.en_title),
|
||||
"en_title": cls.__convert_invalid_characters(mediainfo.en_title),
|
||||
# 原语种标题
|
||||
"original_title": self.__convert_invalid_characters(mediainfo.original_title),
|
||||
"original_title": cls.__convert_invalid_characters(mediainfo.original_title),
|
||||
# 季号
|
||||
"season": self._context.get("season") or mediainfo.season,
|
||||
"season": context.get("season") or mediainfo.season,
|
||||
# Sxx
|
||||
"season_fmt": self._context.get("season_fmt") or season_fmt,
|
||||
"season_fmt": context.get("season_fmt") or season_fmt,
|
||||
# 年份
|
||||
"year": mediainfo.year or self._context.get("year"),
|
||||
"year": mediainfo.year or context.get("year"),
|
||||
# 媒体标题 + 年份
|
||||
"title_year": mediainfo.title_year or self._context.get("title_year"),
|
||||
"title_year": mediainfo.title_year or context.get("title_year"),
|
||||
}
|
||||
|
||||
_meta_season = self._context.get("season")
|
||||
_meta_season = context.get("season")
|
||||
media_info = {
|
||||
# 类型
|
||||
"type": mediainfo.type.value,
|
||||
@@ -121,11 +135,18 @@ class TemplateContextBuilder:
|
||||
# 豆瓣ID
|
||||
"doubanid": mediainfo.douban_id,
|
||||
}
|
||||
self._context.update({**base_info, **media_info})
|
||||
context.update({**base_info, **media_info})
|
||||
|
||||
def _add_episode_details(self, meta: Optional[MetaBase], episodes: Optional[List[TmdbEpisode]]):
|
||||
@classmethod
|
||||
def _add_episode_details(
|
||||
cls,
|
||||
context: Dict[str, Any],
|
||||
meta: Optional[MetaBase],
|
||||
episodes: Optional[List[TmdbEpisode]],
|
||||
) -> None:
|
||||
"""
|
||||
添加剧集详细信息
|
||||
将 meta 解析得到的剧集级信息、技术字段写入 ``context``,并尝试匹配
|
||||
TMDB 集详情填入 ``episode_title`` / ``episode_date``。
|
||||
"""
|
||||
if not meta:
|
||||
return
|
||||
@@ -135,7 +156,7 @@ class TemplateContextBuilder:
|
||||
for episode in episodes:
|
||||
if episode.episode_number == meta.begin_episode:
|
||||
episode_data.update({
|
||||
"episode_title": self.__convert_invalid_characters(episode.name),
|
||||
"episode_title": cls.__convert_invalid_characters(episode.name),
|
||||
"episode_date": episode.air_date if episode.air_date else None
|
||||
})
|
||||
break
|
||||
@@ -150,7 +171,7 @@ class TemplateContextBuilder:
|
||||
# 年份
|
||||
"year": meta.year,
|
||||
# 名字 + 年份
|
||||
"title_year": self._context.get("title_year") or "%s (%s)" % (
|
||||
"title_year": context.get("title_year") or "%s (%s)" % (
|
||||
meta.name, meta.year) if meta.year else meta.name,
|
||||
# 季号
|
||||
"season": meta.season_seq,
|
||||
@@ -188,11 +209,16 @@ class TemplateContextBuilder:
|
||||
# 流媒体平台
|
||||
"webSource": meta.web_source,
|
||||
}
|
||||
self._context.update({**meta_info, **tech_metadata, **episode_data})
|
||||
context.update({**meta_info, **tech_metadata, **episode_data})
|
||||
|
||||
def _add_torrent_info(self, torrentinfo: Optional[TorrentInfo]):
|
||||
@staticmethod
|
||||
def _add_torrent_info(context: Dict[str, Any], torrentinfo: Optional[TorrentInfo]) -> None:
|
||||
"""
|
||||
添加种子信息
|
||||
将种子信息写入 ``context``,描述字段会去除 HTML 标签。
|
||||
|
||||
副作用提醒:当 ``torrentinfo.description`` 包含 HTML 时,会就地清洗
|
||||
原对象的 description 字段——保留原始行为,避免破坏现有调用方对清洗后
|
||||
描述的依赖。
|
||||
"""
|
||||
if not torrentinfo:
|
||||
return
|
||||
@@ -231,25 +257,27 @@ class TemplateContextBuilder:
|
||||
# 种子大小
|
||||
"size": size,
|
||||
}
|
||||
self._context.update(torrent_info)
|
||||
context.update(torrent_info)
|
||||
|
||||
def _add_transfer_info(self, transferinfo: Optional[TransferInfo]) -> Optional[Dict]:
|
||||
@staticmethod
|
||||
def _add_transfer_info(context: Dict[str, Any], transferinfo: Optional[TransferInfo]) -> None:
|
||||
"""
|
||||
添加文件转移上下文
|
||||
将整理结果(类型、文件数、总大小、错误信息)写入 ``context``。
|
||||
"""
|
||||
if not transferinfo:
|
||||
return None
|
||||
return
|
||||
ctx = {
|
||||
"transfer_type": transferinfo.transfer_type,
|
||||
"file_count": transferinfo.file_count,
|
||||
"total_size": StringUtils.str_filesize(transferinfo.total_size),
|
||||
"err_msg": transferinfo.message,
|
||||
}
|
||||
return self._context.update(ctx)
|
||||
context.update(ctx)
|
||||
|
||||
def _add_file_info(self, file_extension: Optional[str]):
|
||||
@staticmethod
|
||||
def _add_file_info(context: Dict[str, Any], file_extension: Optional[str]) -> None:
|
||||
"""
|
||||
添加文件信息
|
||||
将文件扩展名写入 ``context.fileExt``。
|
||||
"""
|
||||
if not file_extension:
|
||||
return
|
||||
@@ -257,18 +285,21 @@ class TemplateContextBuilder:
|
||||
# 文件后缀
|
||||
"fileExt": file_extension,
|
||||
}
|
||||
self._context.update(file_info)
|
||||
context.update(file_info)
|
||||
|
||||
@staticmethod
|
||||
def _add_raw_objects(
|
||||
self,
|
||||
context: Dict[str, Any],
|
||||
meta: Optional[MetaBase],
|
||||
mediainfo: Optional[MediaInfo],
|
||||
torrentinfo: Optional[TorrentInfo],
|
||||
transferinfo: Optional[TransferInfo],
|
||||
episodes_info: Optional[List[TmdbEpisode]],
|
||||
):
|
||||
) -> None:
|
||||
"""
|
||||
添加原始对象引用
|
||||
以双下划线键名将原始对象引用写入 ``context``。
|
||||
|
||||
约定:消费方仅读不写,避免在事件回调里改这些对象污染下游流程。
|
||||
"""
|
||||
raw_objects = {
|
||||
# 文件元数据
|
||||
@@ -282,7 +313,7 @@ class TemplateContextBuilder:
|
||||
# 当前季的全部集信息
|
||||
"__episodes_info__": episodes_info,
|
||||
}
|
||||
self._context.update(raw_objects)
|
||||
context.update(raw_objects)
|
||||
|
||||
@staticmethod
|
||||
def __convert_invalid_characters(filename: str):
|
||||
|
||||
Reference in New Issue
Block a user