mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-03 13:55:00 +08:00
251 lines
9.8 KiB
Python
251 lines
9.8 KiB
Python
"""
|
||
整理历史写入所需的稳定字段投影。
|
||
"""
|
||
|
||
from typing import Any, Optional, TypedDict, Union
|
||
|
||
from app.application.classification.reference import effective_classification_snapshot
|
||
from app.domain.context import MediaInfo, MusicInfo
|
||
from app.domain.meta.metabase import MetaBase
|
||
from app.domain.meta.metamusic import MetaMusic
|
||
from app.schemas.file import FileItem
|
||
from app.schemas.media import resolve_media_identity
|
||
from app.schemas.transfer import TransferInfo
|
||
from app.schemas.types import MUSIC_ENTITY_RECORDING
|
||
|
||
|
||
def classification_fields(
|
||
media: Union[MediaInfo, MusicInfo],
|
||
) -> dict[str, Any]:
|
||
"""
|
||
投影整理历史使用的最终分类标量,避免保存推荐分类。
|
||
"""
|
||
snapshot = effective_classification_snapshot(media)
|
||
return {
|
||
"media_category_id": snapshot.category_id,
|
||
"category": snapshot.path,
|
||
"classification_rule_id": snapshot.rule_id,
|
||
"classification_policy_revision": snapshot.policy_revision,
|
||
"classification_source": snapshot.source,
|
||
}
|
||
|
||
|
||
def history_title(
|
||
meta: Optional[MetaBase],
|
||
mediainfo: Optional[Union[MediaInfo, MusicInfo]] = None,
|
||
) -> Optional[str]:
|
||
"""
|
||
音乐文件优先记录曲目标题,其它媒体保持识别标题;均未识别时不推测标题。
|
||
"""
|
||
if isinstance(meta, MetaMusic) and meta.title:
|
||
return str(meta.title)
|
||
if mediainfo and mediainfo.title:
|
||
return str(mediainfo.title)
|
||
return str(meta.name) if meta and meta.name else None
|
||
|
||
|
||
def history_source_path(fileitem: FileItem) -> str:
|
||
"""
|
||
返回整理历史必需的源路径,拒绝持久化无身份记录。
|
||
"""
|
||
if not fileitem.path:
|
||
raise ValueError("整理历史缺少源文件路径")
|
||
return fileitem.path
|
||
|
||
|
||
def history_year(value: object) -> Optional[str]:
|
||
"""
|
||
把媒体年份规范为整理历史稳定字符串。
|
||
"""
|
||
return str(value) if value is not None else None
|
||
|
||
|
||
def success_fields(
|
||
fileitem: FileItem,
|
||
mode: str,
|
||
meta: MetaBase,
|
||
mediainfo: Union[MediaInfo, MusicInfo],
|
||
transferinfo: TransferInfo,
|
||
downloader: Optional[str] = None,
|
||
download_hash: Optional[str] = None,
|
||
) -> dict[str, Any]:
|
||
"""
|
||
投影转移成功记录,不执行数据库写入。
|
||
:param fileitem: 源文件项
|
||
:param mode: 整理方式
|
||
:param meta: 文件名识别结果
|
||
:param mediainfo: 媒体识别结果
|
||
:param transferinfo: 整理结果
|
||
:param downloader: 下载器
|
||
:param download_hash: 种子 hash
|
||
:return: 可构造类型化写入命令的历史字段
|
||
"""
|
||
media_source, media_id = resolve_media_identity(media=mediainfo)
|
||
return dict(
|
||
src=history_source_path(fileitem),
|
||
src_storage=fileitem.storage,
|
||
src_fileitem=fileitem.model_dump(),
|
||
dest=transferinfo.target_item.path if transferinfo.target_item else None,
|
||
dest_storage=transferinfo.target_item.storage if transferinfo.target_item else None,
|
||
dest_fileitem=transferinfo.target_item.model_dump() if transferinfo.target_item else None,
|
||
mode=mode,
|
||
type=(mediainfo.type.value if mediainfo.type else ""),
|
||
**classification_fields(mediainfo),
|
||
title=history_title(meta, mediainfo),
|
||
year=history_year(mediainfo.year),
|
||
media_source=media_source,
|
||
media_id=media_id,
|
||
music_type=getattr(mediainfo, "music_type", None),
|
||
total_tracks=getattr(mediainfo, "total_tracks", None),
|
||
audio_format=getattr(meta, "audio_format", None),
|
||
audio_lossless=getattr(meta, "audio_lossless", None),
|
||
bit_depth=getattr(meta, "bit_depth", None),
|
||
sample_rate=getattr(meta, "sample_rate", None),
|
||
bitrate=getattr(meta, "bitrate", None),
|
||
seasons=meta.season,
|
||
episodes=meta.episode,
|
||
image=mediainfo.get_poster_image(),
|
||
downloader=downloader,
|
||
download_hash=download_hash,
|
||
episode_group=mediainfo.episode_group,
|
||
status=True,
|
||
retry_count=0,
|
||
auto_paused=False,
|
||
files=transferinfo.file_list,
|
||
)
|
||
|
||
|
||
def failure_fields(
|
||
fileitem: FileItem,
|
||
mode: str,
|
||
meta: Optional[MetaBase],
|
||
mediainfo: Optional[Union[MediaInfo, MusicInfo]] = None,
|
||
transferinfo: Optional[TransferInfo] = None,
|
||
downloader: Optional[str] = None,
|
||
download_hash: Optional[str] = None,
|
||
retry_count: Optional[int] = None,
|
||
auto_paused: bool = False,
|
||
) -> dict[str, Any]:
|
||
"""
|
||
投影转移失败记录,不执行数据库写入。
|
||
|
||
识别结果与整理结果齐备时按完整字段落库;缺任一项则走「未识别到媒体信息」分支,
|
||
此时仅保存已有的文件名元数据,不写目标路径。元数据为空时仍保留源文件与失败原因,
|
||
不推测年份或季集。
|
||
|
||
:param fileitem: 源文件项
|
||
:param mode: 整理方式
|
||
:param meta: 文件名识别结果,未解析时为 None
|
||
:param mediainfo: 媒体识别结果,未识别时为 None
|
||
:param transferinfo: 整理结果,未进入整理时为 None
|
||
:param downloader: 下载器
|
||
:param download_hash: 种子 hash
|
||
:param retry_count: 当前文件版本累计失败次数
|
||
:param auto_paused: 是否已达到自动整理暂停阈值
|
||
:return: 可构造类型化写入命令的历史字段
|
||
"""
|
||
from app.application.transfer.feedback import classify_transfer_failure
|
||
|
||
raw_error = transferinfo.message if transferinfo else "未识别到媒体信息"
|
||
feedback = classify_transfer_failure(
|
||
raw_error,
|
||
overwrite_skipped=bool(transferinfo and transferinfo.overwrite_skipped),
|
||
)
|
||
# 源/目标路径已经是历史的一等字段,errmsg 继续保持旧接口的单条原因文本,
|
||
# 阶段和恢复动作由独立字段承载,避免破坏已有客户端和导出脚本的解析。
|
||
public_error = raw_error or "未知错误"
|
||
if mediainfo and transferinfo:
|
||
media_source, media_id = resolve_media_identity(media=mediainfo)
|
||
history = dict(
|
||
src=history_source_path(fileitem),
|
||
src_storage=fileitem.storage,
|
||
src_fileitem=fileitem.model_dump(),
|
||
dest=transferinfo.target_item.path if transferinfo.target_item else None,
|
||
dest_storage=transferinfo.target_item.storage if transferinfo.target_item else None,
|
||
dest_fileitem=transferinfo.target_item.model_dump() if transferinfo.target_item else None,
|
||
mode=mode,
|
||
type=(mediainfo.type.value if mediainfo.type else ""),
|
||
**classification_fields(mediainfo),
|
||
title=history_title(meta, mediainfo),
|
||
year=history_year(mediainfo.year or (meta.year if meta else None)),
|
||
media_source=media_source,
|
||
media_id=media_id,
|
||
music_type=getattr(mediainfo, "music_type", None),
|
||
total_tracks=getattr(mediainfo, "total_tracks", None),
|
||
audio_format=getattr(meta, "audio_format", None),
|
||
audio_lossless=getattr(meta, "audio_lossless", None),
|
||
bit_depth=getattr(meta, "bit_depth", None),
|
||
sample_rate=getattr(meta, "sample_rate", None),
|
||
bitrate=getattr(meta, "bitrate", None),
|
||
seasons=meta.season if meta else None,
|
||
episodes=meta.episode if meta else None,
|
||
image=mediainfo.get_poster_image(),
|
||
downloader=downloader,
|
||
download_hash=download_hash,
|
||
episode_group=mediainfo.episode_group,
|
||
status=False,
|
||
errmsg=public_error,
|
||
failure_stage=transferinfo.failure_stage or feedback.stage.value,
|
||
recovery_action=transferinfo.recovery_action or feedback.action,
|
||
retry_count=retry_count,
|
||
auto_paused=auto_paused,
|
||
cleanup_status=transferinfo.cleanup_status,
|
||
cleanup_error=transferinfo.cleanup_error,
|
||
files=transferinfo.file_list,
|
||
)
|
||
else:
|
||
media_source, media_id = resolve_media_identity(media=meta)
|
||
history = dict(
|
||
type=meta.type.value if meta and meta.type else None,
|
||
title=history_title(meta),
|
||
year=history_year(meta.year if meta else None),
|
||
media_source=media_source,
|
||
media_id=media_id,
|
||
music_type=MUSIC_ENTITY_RECORDING if isinstance(meta, MetaMusic) else None,
|
||
audio_format=getattr(meta, "audio_format", None),
|
||
audio_lossless=getattr(meta, "audio_lossless", None),
|
||
bit_depth=getattr(meta, "bit_depth", None),
|
||
sample_rate=getattr(meta, "sample_rate", None),
|
||
bitrate=getattr(meta, "bitrate", None),
|
||
src=history_source_path(fileitem),
|
||
src_storage=fileitem.storage,
|
||
src_fileitem=fileitem.model_dump(),
|
||
mode=mode,
|
||
seasons=meta.season if meta else None,
|
||
episodes=meta.episode if meta else None,
|
||
downloader=downloader,
|
||
download_hash=download_hash,
|
||
status=False,
|
||
errmsg=public_error,
|
||
failure_stage=feedback.stage.value,
|
||
recovery_action=feedback.action,
|
||
retry_count=retry_count,
|
||
auto_paused=auto_paused,
|
||
)
|
||
return history
|
||
|
||
|
||
class HistoryFileFingerprint(TypedDict):
|
||
"""
|
||
查重描述与裁决共享的源文件版本字段。
|
||
|
||
:ivar file_size: 源文件大小,单位为字节;未知时为 None
|
||
:ivar file_modify_time: 源文件修改时间戳,单位为秒;未知时为 None
|
||
:ivar fileid: 存储后端提供的稳定文件标识;未提供时为 None
|
||
"""
|
||
|
||
file_size: Optional[int]
|
||
file_modify_time: Optional[float]
|
||
fileid: Optional[str]
|
||
|
||
|
||
def history_file_fingerprint(fileitem: FileItem) -> HistoryFileFingerprint:
|
||
"""
|
||
从同一个文件快照取出查重所需的版本指纹。
|
||
"""
|
||
return {
|
||
"file_size": fileitem.size,
|
||
"file_modify_time": fileitem.modify_time,
|
||
"fileid": fileitem.fileid,
|
||
}
|