mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-07 00:16:57 +08:00
fix: add total episodes to transfer notifications
This commit is contained in:
@@ -40,6 +40,7 @@ from app.schemas import (
|
|||||||
TransferQueue,
|
TransferQueue,
|
||||||
TransferJob,
|
TransferJob,
|
||||||
TransferJobTask,
|
TransferJobTask,
|
||||||
|
TmdbEpisode,
|
||||||
)
|
)
|
||||||
from app.schemas.exception import OperationInterrupted
|
from app.schemas.exception import OperationInterrupted
|
||||||
from app.schemas.types import (
|
from app.schemas.types import (
|
||||||
@@ -947,6 +948,7 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
|||||||
mediainfo=task.mediainfo,
|
mediainfo=task.mediainfo,
|
||||||
transferinfo=transferinfo,
|
transferinfo=transferinfo,
|
||||||
season_episode=se_str,
|
season_episode=se_str,
|
||||||
|
episodes_info=task.episodes_info,
|
||||||
username=task.username,
|
username=task.username,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3395,10 +3397,17 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
|||||||
mediainfo: MediaInfo,
|
mediainfo: MediaInfo,
|
||||||
transferinfo: TransferInfo,
|
transferinfo: TransferInfo,
|
||||||
season_episode: Optional[str] = None,
|
season_episode: Optional[str] = None,
|
||||||
|
episodes_info: Optional[List[TmdbEpisode]] = None,
|
||||||
username: Optional[str] = None,
|
username: Optional[str] = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
发送入库成功的消息
|
发送入库成功的消息
|
||||||
|
:param meta: 文件元数据
|
||||||
|
:param mediainfo: 识别的媒体信息
|
||||||
|
:param transferinfo: 文件整理信息
|
||||||
|
:param season_episode: 已入库季集文本
|
||||||
|
:param episodes_info: 当前季的全部集信息
|
||||||
|
:param username: 用户名
|
||||||
"""
|
"""
|
||||||
self.post_message(
|
self.post_message(
|
||||||
Notification(
|
Notification(
|
||||||
@@ -3412,6 +3421,7 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
|||||||
mediainfo=mediainfo,
|
mediainfo=mediainfo,
|
||||||
transferinfo=transferinfo,
|
transferinfo=transferinfo,
|
||||||
season_episode=season_episode,
|
season_episode=season_episode,
|
||||||
|
episodes_info=episodes_info,
|
||||||
username=username,
|
username=username,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -180,6 +180,8 @@ class TemplateContextBuilder:
|
|||||||
"season_fmt": meta.season,
|
"season_fmt": meta.season,
|
||||||
# 集号
|
# 集号
|
||||||
"episode": meta.episode_seqs,
|
"episode": meta.episode_seqs,
|
||||||
|
# 当前季总集数
|
||||||
|
"total_episodes": len(episodes) if episodes else 0,
|
||||||
# 季集 SxxExx
|
# 季集 SxxExx
|
||||||
"season_episode": "%s%s" % (meta.season, meta.episode),
|
"season_episode": "%s%s" % (meta.season, meta.episode),
|
||||||
# 段/节
|
# 段/节
|
||||||
|
|||||||
@@ -9,67 +9,18 @@ TemplateContextBuilder 的并发安全单元测试。
|
|||||||
线程下连续调用 ``build()``,校验每个线程拿到的字典只反映自己的入参。
|
线程下连续调用 ``build()``,校验每个线程拿到的字典只反映自己的入参。
|
||||||
"""
|
"""
|
||||||
import threading
|
import threading
|
||||||
import unittest
|
|
||||||
|
|
||||||
from app.helper.message import TemplateContextBuilder
|
from app.helper.message import TemplateContextBuilder
|
||||||
|
from app.schemas.tmdb import TmdbEpisode
|
||||||
|
|
||||||
|
|
||||||
class TemplateContextBuilderConcurrencyTest(unittest.TestCase):
|
|
||||||
"""
|
|
||||||
使用 8 个线程并发调用同一 TemplateContextBuilder 实例的 build(),
|
|
||||||
确保各自的 file_extension / 自定义 kwargs 不会被其它线程覆盖。
|
|
||||||
"""
|
|
||||||
|
|
||||||
THREAD_COUNT = 8
|
THREAD_COUNT = 8
|
||||||
ITERATIONS_PER_THREAD = 200
|
ITERATIONS_PER_THREAD = 200
|
||||||
|
|
||||||
def test_concurrent_build_no_cross_contamination(self):
|
|
||||||
builder = TemplateContextBuilder()
|
|
||||||
errors = []
|
|
||||||
|
|
||||||
def worker(tag: int) -> None:
|
def _build_fake_meta():
|
||||||
try:
|
|
||||||
for _ in range(self.ITERATIONS_PER_THREAD):
|
|
||||||
ctx = builder.build(
|
|
||||||
file_extension=f".{tag}",
|
|
||||||
marker=tag,
|
|
||||||
)
|
|
||||||
self.assertEqual(ctx.get("fileExt"), f".{tag}")
|
|
||||||
self.assertEqual(ctx.get("marker"), tag)
|
|
||||||
except AssertionError as exc:
|
|
||||||
errors.append(exc)
|
|
||||||
|
|
||||||
threads = [
|
|
||||||
threading.Thread(target=worker, args=(i,), name=f"builder-{i}")
|
|
||||||
for i in range(self.THREAD_COUNT)
|
|
||||||
]
|
|
||||||
for t in threads:
|
|
||||||
t.start()
|
|
||||||
for t in threads:
|
|
||||||
t.join()
|
|
||||||
|
|
||||||
self.assertFalse(
|
|
||||||
errors,
|
|
||||||
msg=f"检测到并发串味,共 {len(errors)} 条;首个错误:{errors[0] if errors else ''}",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_build_returns_independent_dicts(self):
|
|
||||||
"""
|
"""
|
||||||
即便不开线程,连续两次 build() 也应当返回相互独立的 dict 实例,
|
构造模板上下文测试所需的最小元数据对象。
|
||||||
避免无状态化后调用方误以为返回的还是 builder 内部共享对象。
|
|
||||||
"""
|
|
||||||
builder = TemplateContextBuilder()
|
|
||||||
first = builder.build(file_extension=".a", marker=1)
|
|
||||||
second = builder.build(file_extension=".b", marker=2)
|
|
||||||
self.assertIsNot(first, second)
|
|
||||||
self.assertEqual(first.get("fileExt"), ".a")
|
|
||||||
self.assertEqual(second.get("fileExt"), ".b")
|
|
||||||
# 第二次调用不应反向污染第一次的结果
|
|
||||||
self.assertEqual(first.get("marker"), 1)
|
|
||||||
|
|
||||||
def test_build_exposes_video_bit_from_meta(self):
|
|
||||||
"""
|
|
||||||
模板上下文应提供独立 videoBit 字段,避免用户只能从 videoCodec 中手工拆位深。
|
|
||||||
"""
|
"""
|
||||||
meta = type("FakeMeta", (), {})()
|
meta = type("FakeMeta", (), {})()
|
||||||
meta.begin_episode = None
|
meta.begin_episode = None
|
||||||
@@ -94,8 +45,76 @@ class TemplateContextBuilderConcurrencyTest(unittest.TestCase):
|
|||||||
meta.video_bit = "10bit"
|
meta.video_bit = "10bit"
|
||||||
meta.audio_encode = "AAC"
|
meta.audio_encode = "AAC"
|
||||||
meta.web_source = None
|
meta.web_source = None
|
||||||
|
return meta
|
||||||
|
|
||||||
context = TemplateContextBuilder().build(meta=meta)
|
|
||||||
|
|
||||||
self.assertEqual(context.get("videoCodec"), "x265 10bit")
|
def test_concurrent_build_no_cross_contamination() -> None:
|
||||||
self.assertEqual(context.get("videoBit"), "10bit")
|
"""
|
||||||
|
使用 8 个线程并发调用同一 TemplateContextBuilder 实例的 build(),
|
||||||
|
确保各自的 file_extension / 自定义 kwargs 不会被其它线程覆盖。
|
||||||
|
"""
|
||||||
|
builder = TemplateContextBuilder()
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
def worker(tag: int) -> None:
|
||||||
|
try:
|
||||||
|
for _ in range(ITERATIONS_PER_THREAD):
|
||||||
|
ctx = builder.build(
|
||||||
|
file_extension=f".{tag}",
|
||||||
|
marker=tag,
|
||||||
|
)
|
||||||
|
assert ctx.get("fileExt") == f".{tag}"
|
||||||
|
assert ctx.get("marker") == tag
|
||||||
|
except AssertionError as exc:
|
||||||
|
errors.append(exc)
|
||||||
|
|
||||||
|
threads = [
|
||||||
|
threading.Thread(target=worker, args=(i,), name=f"builder-{i}")
|
||||||
|
for i in range(THREAD_COUNT)
|
||||||
|
]
|
||||||
|
for thread in threads:
|
||||||
|
thread.start()
|
||||||
|
for thread in threads:
|
||||||
|
thread.join()
|
||||||
|
|
||||||
|
assert not errors, f"检测到并发串味,共 {len(errors)} 条;首个错误:{errors[0] if errors else ''}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_returns_independent_dicts() -> None:
|
||||||
|
"""
|
||||||
|
连续两次 build() 应返回相互独立的 dict 实例,避免调用方误用共享结果。
|
||||||
|
"""
|
||||||
|
builder = TemplateContextBuilder()
|
||||||
|
first = builder.build(file_extension=".a", marker=1)
|
||||||
|
second = builder.build(file_extension=".b", marker=2)
|
||||||
|
|
||||||
|
assert first is not second
|
||||||
|
assert first.get("fileExt") == ".a"
|
||||||
|
assert second.get("fileExt") == ".b"
|
||||||
|
assert first.get("marker") == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_exposes_video_bit_from_meta() -> None:
|
||||||
|
"""
|
||||||
|
模板上下文应提供独立 videoBit 字段,避免用户只能从 videoCodec 中手工拆位深。
|
||||||
|
"""
|
||||||
|
context = TemplateContextBuilder().build(meta=_build_fake_meta())
|
||||||
|
|
||||||
|
assert context.get("videoCodec") == "x265 10bit"
|
||||||
|
assert context.get("videoBit") == "10bit"
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_exposes_total_episodes_from_current_season() -> None:
|
||||||
|
"""
|
||||||
|
模板上下文应提供当前季总集数,供入库通知模板直接引用。
|
||||||
|
"""
|
||||||
|
context = TemplateContextBuilder().build(
|
||||||
|
meta=_build_fake_meta(),
|
||||||
|
episodes_info=[
|
||||||
|
TmdbEpisode(episode_number=1, name="第一集"),
|
||||||
|
TmdbEpisode(episode_number=2, name="第二集"),
|
||||||
|
TmdbEpisode(episode_number=3, name="第三集"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert context.get("total_episodes") == 3
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from app.chain.transfer import TransferChain
|
||||||
|
from app.core.context import MediaInfo
|
||||||
|
from app.core.meta.metabase import MetaBase
|
||||||
|
from app.schemas import TransferInfo
|
||||||
|
from app.schemas.tmdb import TmdbEpisode
|
||||||
|
from app.schemas.types import ContentType, MediaType, NotificationType
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_transfer_message_passes_episode_info_to_template_context() -> None:
|
||||||
|
"""
|
||||||
|
入库成功通知应把当前季集信息传给消息模板,确保 total_episodes 可渲染。
|
||||||
|
"""
|
||||||
|
chain = TransferChain()
|
||||||
|
meta = MetaBase("Test.Show.S01E01.mkv")
|
||||||
|
meta.type = MediaType.TV
|
||||||
|
meta.name = "Test Show"
|
||||||
|
meta.begin_season = 1
|
||||||
|
meta.begin_episode = 1
|
||||||
|
episodes_info = [
|
||||||
|
TmdbEpisode(episode_number=1, name="第一集"),
|
||||||
|
TmdbEpisode(episode_number=2, name="第二集"),
|
||||||
|
]
|
||||||
|
mediainfo = MediaInfo(
|
||||||
|
type=MediaType.TV,
|
||||||
|
title="Test Show",
|
||||||
|
season=1,
|
||||||
|
tmdb_id=12345,
|
||||||
|
)
|
||||||
|
transferinfo = TransferInfo(success=True)
|
||||||
|
|
||||||
|
with patch.object(chain, "post_message") as post_message:
|
||||||
|
chain.send_transfer_message(
|
||||||
|
meta=meta,
|
||||||
|
mediainfo=mediainfo,
|
||||||
|
transferinfo=transferinfo,
|
||||||
|
season_episode="S01 E01",
|
||||||
|
episodes_info=episodes_info,
|
||||||
|
username="tester",
|
||||||
|
)
|
||||||
|
|
||||||
|
message = post_message.call_args.args[0]
|
||||||
|
assert message.mtype == NotificationType.Organize
|
||||||
|
assert message.ctype == ContentType.OrganizeSuccess
|
||||||
|
assert post_message.call_args.kwargs["episodes_info"] is episodes_info
|
||||||
|
assert post_message.call_args.kwargs["season_episode"] == "S01 E01"
|
||||||
Reference in New Issue
Block a user