Files
MoviePilot/tests/test_transfer_history_write_path.py

292 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
整理历史的写入路径add_transfer_success / add_transfer_fail。
这两个函数是整理历史表的**唯一**写入口,整理链的每一次成败都经由它们落库。
它们干的不是数据访问,而是把 FileItem / MetaBase / MediaInfo / TransferInfo
四个领域对象翻译成一行历史——字段映射错了不会报错,只会让历史里的季集、标题、
来源静静地记错,而整理查重、媒体库溯源、失败重试全都读这张表。
因此这里断言的是「落库后每个字段的实际值」,不是「调用了什么」。
它们与同一张表的读侧规则(查重闸,见 test_transfer_history_gate.py同住
app/application/history.py此前长在 TransferHistoryOper 上,故本文件旧名为
test_db_transferhistory_write_path.py。
"""
import pytest
from app import schemas
from app.application.history import add_transfer_fail, add_transfer_success
from app.domain.context import MediaInfo
from app.domain.metainfo import MetaInfo
from app.db.models.transferhistory import TransferHistory
from app.db.oper.transferhistory import TransferHistoryOper
from app.schemas.types import MediaSource, MediaType
@pytest.fixture(autouse=True)
def _track(db):
"""把整理历史表纳入用例级回收。"""
db.watermark(TransferHistory)
def _fileitem(path: str, storage: str = "local") -> schemas.FileItem:
"""构造源文件项。"""
return schemas.FileItem(storage=storage, path=path,
name=path.rsplit("/", 1)[-1], type="file")
def _transferinfo(dest: str = "/media/片名/Season 01/片名 - S01E02.mkv",
message: str = None, with_target: bool = True) -> schemas.TransferInfo:
"""构造整理结果。"""
target = _fileitem(dest) if with_target else None
return schemas.TransferInfo(success=not message,
fileitem=_fileitem("/downloads/片名.S01E02.2026.mkv"),
target_item=target,
file_list=["/downloads/片名.S01E02.2026.mkv"],
message=message)
def _mediainfo(title: str = "识别标题", mtype: MediaType = MediaType.TV,
year: str = "2026", category: str = "国产剧",
media_id: str = "5566", episode_group: str = None) -> MediaInfo:
"""构造识别结果。"""
media = MediaInfo()
media.type = mtype
media.title = title
media.year = year
media.category = category
media.media_source = MediaSource.TMDB
media.media_id = media_id
media.episode_group = episode_group
return media
# --------------------------------------------------------------------------- #
# add_transfer_success
# --------------------------------------------------------------------------- #
def test_add_success_maps_every_field_onto_the_row(db):
"""
成功整理的字段映射必须完整落库。
源/目标各自带存储、季集来自识别结果、状态为成功——这些是整理查重与媒体库
溯源的全部依据,错一项就查不回来。
"""
oper = TransferHistoryOper(db=db.session)
meta = MetaInfo("片名.S01E02.2026.mkv")
add_transfer_success(transfer_history_oper=oper,
fileitem=_fileitem("/downloads/片名.S01E02.2026.mkv"),
mode="move", meta=meta, mediainfo=_mediainfo(),
transferinfo=_transferinfo(),
downloader="qbittorrent", download_hash="hash-ok")
row = oper.get_by_src("/downloads/片名.S01E02.2026.mkv")
assert row.status is True
assert (row.src_storage, row.dest_storage) == ("local", "local")
assert row.dest == "/media/片名/Season 01/片名 - S01E02.mkv"
assert (row.mode, row.type, row.category) == ("move", MediaType.TV.value, "国产剧")
assert (row.title, row.year) == ("识别标题", "2026")
assert (row.seasons, row.episodes) == ("S01", "E02")
assert (row.downloader, row.download_hash) == ("qbittorrent", "hash-ok")
assert row.files == ["/downloads/片名.S01E02.2026.mkv"]
assert row.errmsg is None
def test_add_success_persists_both_file_items(db):
"""
源与目标的完整文件项都要落库。
整理链回滚、重新整理都要拿原始 FileItem 复原,只存路径字符串不够。
"""
oper = TransferHistoryOper(db=db.session)
add_transfer_success(transfer_history_oper=oper,
fileitem=_fileitem("/downloads/item.mkv"), mode="link",
meta=MetaInfo("item.mkv"), mediainfo=_mediainfo(),
transferinfo=_transferinfo(dest="/media/item.mkv"))
row = oper.get_by_src("/downloads/item.mkv")
assert row.src_fileitem["path"] == "/downloads/item.mkv"
assert row.dest_fileitem["path"] == "/media/item.mkv"
def test_add_success_tolerates_missing_target_item(db):
"""
没有目标文件项时,目标三项一并留空而不是崩。
某些存储的整理只回传结果不回传条目,硬取 target_item.path 会直接抛。
"""
oper = TransferHistoryOper(db=db.session)
add_transfer_success(transfer_history_oper=oper,
fileitem=_fileitem("/downloads/no-target.mkv"), mode="copy",
meta=MetaInfo("no-target.mkv"), mediainfo=_mediainfo(),
transferinfo=_transferinfo(with_target=False))
row = oper.get_by_src("/downloads/no-target.mkv")
assert (row.dest, row.dest_storage, row.dest_fileitem) == (None, None, None)
assert row.status is True
def test_add_success_replaces_the_previous_record_of_the_same_source(db):
"""
同一源路径重复整理只保留最新一条。
经 add_force 的先删后插实现;留下旧行会让查重命中过期的目标路径。
"""
oper = TransferHistoryOper(db=db.session)
for title in ("第一次", "第二次"):
add_transfer_success(transfer_history_oper=oper,
fileitem=_fileitem("/downloads/dup.mkv"), mode="move",
meta=MetaInfo("dup.mkv"),
mediainfo=_mediainfo(title=title),
transferinfo=_transferinfo())
rows = oper.list_success_by_src("/downloads/dup.mkv")
assert [r.title for r in rows] == ["第二次"]
def test_add_success_prefers_track_title_for_music(db):
"""
音乐文件记录曲目标题,而不是识别出的专辑/艺人名。
音乐库按曲目组织,记成专辑名会让单曲在历史里全部重名、无法区分。
"""
oper = TransferHistoryOper(db=db.session)
music_meta = MetaInfo("周杰伦 - 晴天.flac")
add_transfer_success(transfer_history_oper=oper,
fileitem=_fileitem("/downloads/晴天.flac"), mode="move",
meta=music_meta,
mediainfo=_mediainfo(title="叶惠美", mtype=MediaType.MUSIC),
transferinfo=_transferinfo(dest="/media/晴天.flac"))
assert oper.get_by_src("/downloads/晴天.flac").title == "晴天"
def test_add_success_falls_back_to_recognized_title(db):
"""
非音乐文件用识别标题,识别标题为空时退回文件名解析出的名字。
"""
oper = TransferHistoryOper(db=db.session)
meta = MetaInfo("片名.S01E02.2026.mkv")
add_transfer_success(transfer_history_oper=oper,
fileitem=_fileitem("/downloads/fallback.mkv"), mode="move",
meta=meta, mediainfo=_mediainfo(title=None),
transferinfo=_transferinfo())
assert oper.get_by_src("/downloads/fallback.mkv").title == meta.name
# --------------------------------------------------------------------------- #
# add_transfer_fail
# --------------------------------------------------------------------------- #
def test_add_fail_records_the_transfer_error_message(db):
"""
整理失败时状态为失败,并保留具体错误信息。
失败重试与人工排障都只能看这条 errmsg。
"""
oper = TransferHistoryOper(db=db.session)
add_transfer_fail(transfer_history_oper=oper,
fileitem=_fileitem("/downloads/fail.mkv"), mode="move",
meta=MetaInfo("fail.mkv"), mediainfo=_mediainfo(),
transferinfo=_transferinfo(message="目标路径不可写"))
row = oper.get_by_src("/downloads/fail.mkv")
assert row.status is False
assert row.errmsg == "目标路径不可写"
assert row.title == "识别标题"
def test_add_fail_uses_a_default_message_when_none_given(db):
"""
整理结果没带错误信息时落一个兜底文案,不能留空。
留空会让失败记录在界面上显示成「无错误」,与成功记录无从区分。
"""
oper = TransferHistoryOper(db=db.session)
add_transfer_fail(transfer_history_oper=oper,
fileitem=_fileitem("/downloads/nomsg.mkv"), mode="move",
meta=MetaInfo("nomsg.mkv"), mediainfo=_mediainfo(),
transferinfo=_transferinfo(message=""))
assert oper.get_by_src("/downloads/nomsg.mkv").errmsg == "未知错误"
def test_add_fail_persists_episode_group(db):
"""
失败记录要带上剧集组——重试时靠它还原到同一个剧集组,否则会整理错季。
"""
oper = TransferHistoryOper(db=db.session)
add_transfer_fail(transfer_history_oper=oper,
fileitem=_fileitem("/downloads/eg.mkv"), mode="move",
meta=MetaInfo("eg.mkv"),
mediainfo=_mediainfo(episode_group="eg-1"),
transferinfo=_transferinfo(message="出错"))
assert oper.get_by_src("/downloads/eg.mkv").episode_group == "eg-1"
def test_add_fail_without_recognition_takes_the_unidentified_branch(db):
"""
未识别到媒体信息时走另一条分支:错误文案固定,且不写目标路径。
这条分支是「文件识别不出来」的唯一记录方式,丢了这些文件就彻底无迹可寻。
"""
oper = TransferHistoryOper(db=db.session)
meta = MetaInfo("无法识别的文件.S02E05.2020.mkv")
add_transfer_fail(transfer_history_oper=oper,
fileitem=_fileitem("/downloads/unknown.mkv"), mode="move",
meta=meta)
row = oper.get_by_src("/downloads/unknown.mkv")
assert row.status is False
assert row.errmsg == "未识别到媒体信息"
assert row.dest is None
assert (row.seasons, row.episodes) == ("S02", "E05")
assert row.year == meta.year
assert row.title == meta.name
def test_add_fail_unidentified_music_is_marked_as_a_recording(db):
"""
未识别的音乐文件按单曲登记实体类型。
音乐订阅按单曲/专辑分别查重,实体类型为空会让这条历史两边都匹配不上。
"""
oper = TransferHistoryOper(db=db.session)
add_transfer_fail(transfer_history_oper=oper,
fileitem=_fileitem("/downloads/unknown.flac"), mode="move",
meta=MetaInfo("周杰伦 - 晴天.flac"))
row = oper.get_by_src("/downloads/unknown.flac")
assert row.music_type == "recording"
assert row.type == MediaType.MUSIC.value
assert row.title == "晴天"
def test_add_fail_returns_the_persisted_row(db):
"""
两条分支都要把落库后的记录返回,调用方据此拿主键做后续关联。
"""
oper = TransferHistoryOper(db=db.session)
identified = add_transfer_fail(transfer_history_oper=oper,
fileitem=_fileitem("/downloads/r1.mkv"), mode="move",
meta=MetaInfo("r1.mkv"), mediainfo=_mediainfo(),
transferinfo=_transferinfo(message="错误"))
unidentified = add_transfer_fail(transfer_history_oper=oper,
fileitem=_fileitem("/downloads/r2.mkv"), mode="move",
meta=MetaInfo("r2.mkv"))
assert identified.id is not None
assert unidentified.id is not None
assert identified.id != unidentified.id