mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-15 19:14:01 +08:00
292 lines
12 KiB
Python
292 lines
12 KiB
Python
"""
|
||
整理历史的写入路径: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
|