fix: add total episodes to transfer notifications

This commit is contained in:
jxxghp
2026-06-27 22:45:17 +08:00
parent a9197c434e
commit 27e1f634cb
4 changed files with 151 additions and 73 deletions

View File

@@ -9,93 +9,112 @@ TemplateContextBuilder 的并发安全单元测试。
线程下连续调用 ``build()``,校验每个线程拿到的字典只反映自己的入参。
"""
import threading
import unittest
from app.helper.message import TemplateContextBuilder
from app.schemas.tmdb import TmdbEpisode
class TemplateContextBuilderConcurrencyTest(unittest.TestCase):
THREAD_COUNT = 8
ITERATIONS_PER_THREAD = 200
def _build_fake_meta():
"""
构造模板上下文测试所需的最小元数据对象。
"""
meta = type("FakeMeta", (), {})()
meta.begin_episode = None
meta.title = "Movie.2024.1080p.x265.10bit.mkv"
meta.name = "Movie"
meta.en_name = "Movie"
meta.year = "2024"
meta.season_seq = ""
meta.season = ""
meta.episode_seqs = ""
meta.episode = ""
meta.part = None
meta.customization = None
meta.fps = None
meta.resource_type = None
meta.resource_effect = None
meta.edition = ""
meta.resource_pix = "1080p"
meta.resource_term = "1080p"
meta.resource_team = None
meta.video_encode = "x265 10bit"
meta.video_bit = "10bit"
meta.audio_encode = "AAC"
meta.web_source = None
return meta
def test_concurrent_build_no_cross_contamination() -> None:
"""
使用 8 个线程并发调用同一 TemplateContextBuilder 实例的 build()
确保各自的 file_extension / 自定义 kwargs 不会被其它线程覆盖。
"""
builder = TemplateContextBuilder()
errors = []
THREAD_COUNT = 8
ITERATIONS_PER_THREAD = 200
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)
def test_concurrent_build_no_cross_contamination(self):
builder = TemplateContextBuilder()
errors = []
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()
def worker(tag: int) -> None:
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)
assert not errors, f"检测到并发串味,共 {len(errors)} 条;首个错误:{errors[0] if errors else ''}"
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() -> None:
"""
连续两次 build() 应返回相互独立的 dict 实例,避免调用方误用共享结果。
"""
builder = TemplateContextBuilder()
first = builder.build(file_extension=".a", marker=1)
second = builder.build(file_extension=".b", marker=2)
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)
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(self):
"""
模板上下文应提供独立 videoBit 字段,避免用户只能从 videoCodec 中手工拆位深。
"""
meta = type("FakeMeta", (), {})()
meta.begin_episode = None
meta.title = "Movie.2024.1080p.x265.10bit.mkv"
meta.name = "Movie"
meta.en_name = "Movie"
meta.year = "2024"
meta.season_seq = ""
meta.season = ""
meta.episode_seqs = ""
meta.episode = ""
meta.part = None
meta.customization = None
meta.fps = None
meta.resource_type = None
meta.resource_effect = None
meta.edition = ""
meta.resource_pix = "1080p"
meta.resource_term = "1080p"
meta.resource_team = None
meta.video_encode = "x265 10bit"
meta.video_bit = "10bit"
meta.audio_encode = "AAC"
meta.web_source = None
context = TemplateContextBuilder().build(meta=meta)
def test_build_exposes_video_bit_from_meta() -> None:
"""
模板上下文应提供独立 videoBit 字段,避免用户只能从 videoCodec 中手工拆位深。
"""
context = TemplateContextBuilder().build(meta=_build_fake_meta())
self.assertEqual(context.get("videoCodec"), "x265 10bit")
self.assertEqual(context.get("videoBit"), "10bit")
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