mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-01 12:56:34 +08:00
752 lines
26 KiB
Python
752 lines
26 KiB
Python
"""
|
||
下载和整理历史的稳定数据、仓储端口与查询服务。
|
||
|
||
跨层传递历史快照;数据库会话与事务实现由持久化适配器负责。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import asdict, dataclass
|
||
from typing import Any, Optional, Protocol, Union
|
||
|
||
from app.application.history.contracts import (
|
||
TransferHistoryMonthlyStatistics,
|
||
TransferHistoryQueryPort,
|
||
TransferHistoryReplacePort,
|
||
TransferHistoryRepository,
|
||
TransferHistorySnapshot,
|
||
TransferHistoryStagingPort,
|
||
TransferHistoryStatisticSnapshot,
|
||
TransferHistoryWrite,
|
||
TransferHistoryWritePort,
|
||
_freeze_json,
|
||
configure_transfer_history_repository,
|
||
get_transfer_history_repository,
|
||
reset_transfer_history_repository,
|
||
)
|
||
from app.application.history.mutation import (
|
||
DownloadFileMutationRepository as DownloadFileMutationRepository,
|
||
)
|
||
from app.application.history.mutation import (
|
||
DownloadHistoryMutationCommand as DownloadHistoryMutationCommand,
|
||
)
|
||
from app.application.history.mutation import (
|
||
DownloadHistoryMutationRepository as DownloadHistoryMutationRepository,
|
||
)
|
||
from app.application.history.mutation import (
|
||
HistoryMutationResult as HistoryMutationResult,
|
||
)
|
||
from app.application.history.mutation import (
|
||
HistoryUnitOfWork as HistoryUnitOfWork,
|
||
)
|
||
from app.application.history.mutation import (
|
||
TransferHistoryMutationCommand as TransferHistoryMutationCommand,
|
||
)
|
||
from app.application.history.mutation import (
|
||
TransferHistoryMutationRepository as TransferHistoryMutationRepository,
|
||
)
|
||
from app.application.transfer import history as history_projection
|
||
from app.domain.context import MediaInfo, MusicInfo
|
||
from app.domain.meta.metabase import MetaBase
|
||
from app.foundation.text import cut as jieba_cut
|
||
from app.schemas.common import JsonData
|
||
from app.schemas.file import FileItem
|
||
from app.schemas.history import (
|
||
DownloadHistory,
|
||
TransferHistory,
|
||
TransferHistoryPage,
|
||
)
|
||
from app.schemas.transfer import TransferInfo
|
||
from app.schemas.types import MediaSource
|
||
|
||
__all__ = [
|
||
"DownloadFileMutationRepository",
|
||
"DownloadFileSnapshot",
|
||
"DownloadFileWrite",
|
||
"DownloadHistory",
|
||
"DownloadHistoryMutationCommand",
|
||
"DownloadHistoryMutationRepository",
|
||
"DownloadHistoryQueryPort",
|
||
"DownloadHistoryRepository",
|
||
"DownloadHistorySnapshot",
|
||
"DownloadHistoryWrite",
|
||
"DownloadHistoryWritePort",
|
||
"HistoryMutationResult",
|
||
"HistoryQueryService",
|
||
"HistoryUnitOfWork",
|
||
"ManualTransferHistory",
|
||
"TransferHistory",
|
||
"TransferHistoryLookupRepository",
|
||
"TransferHistoryLookupService",
|
||
"TransferHistoryMonthlyStatistics",
|
||
"TransferHistoryMutationCommand",
|
||
"TransferHistoryPage",
|
||
"TransferHistoryQueryPort",
|
||
"TransferHistoryReplacePort",
|
||
"TransferHistoryRepository",
|
||
"TransferHistorySnapshot",
|
||
"TransferHistoryStagingPort",
|
||
"TransferHistoryStatisticSnapshot",
|
||
"TransferHistoryWrite",
|
||
"TransferHistoryWritePort",
|
||
"add_transfer_fail",
|
||
"add_transfer_success",
|
||
"configure_transfer_history_repository",
|
||
"get_transfer_history_repository",
|
||
"reset_transfer_history_repository",
|
||
]
|
||
|
||
|
||
@dataclass(frozen=True, slots=True)
|
||
class DownloadHistorySnapshot:
|
||
"""
|
||
脱离数据库会话后供宿主下载、订阅和整理用例读取的历史快照。
|
||
|
||
:param id: 下载历史记录的数据库主键
|
||
:param path: 下载任务的保存路径
|
||
:param type: 媒体类型,区分电影、电视剧和音乐
|
||
:param title: 识别出的标准媒体标题
|
||
:param year: 媒体发行年份;未知时为 None
|
||
:param media_source: 标准媒体来源,与 media_id 配对使用
|
||
:param media_id: 当前来源中的原生媒体标识
|
||
:param music_type: 音乐实体类型,区分单曲录音与专辑
|
||
:param seasons: 已识别的季号或季范围文本
|
||
:param episodes: 识别出的集号或集范围文本
|
||
:param image: 通知和历史展示使用的图片地址
|
||
:param poster: 媒体海报地址
|
||
:param downloader: 下载器配置名称
|
||
:param download_hash: 下载器中的任务哈希标识
|
||
:param torrent_name: 种子原始标题
|
||
:param torrent_description: 种子副标题或简介
|
||
:param torrent_site: 种子所属站点名称
|
||
:param userid: 触发下载的消息渠道用户标识
|
||
:param username: 触发下载的本地用户名
|
||
:param channel: 触发下载的消息渠道名称
|
||
:param date: 历史记录的时间文本
|
||
:param note: 冻结的附加 JSON 信息,包含恢复音乐上下文所需的扩展数据
|
||
:param media_category_id: 媒体类别的稳定标识
|
||
:param media_category: 下载时保存的媒体类别路径快照
|
||
:param classification_rule_id: 当次分类命中的规则标识
|
||
:param classification_policy_revision: 当次执行使用的分类策略版本
|
||
:param classification_source: 当次最终分类的依据来源
|
||
:param episode_group: 当前媒体使用的剧集组标识
|
||
:param custom_words: 该任务专用的自定义识别词
|
||
"""
|
||
|
||
id: int
|
||
path: str
|
||
type: str
|
||
title: str
|
||
year: Optional[str] = None
|
||
media_source: Optional[MediaSource] = None
|
||
media_id: Optional[str] = None
|
||
music_type: Optional[str] = None
|
||
seasons: Optional[str] = None
|
||
episodes: Optional[str] = None
|
||
image: Optional[str] = None
|
||
poster: Optional[str] = None
|
||
downloader: Optional[str] = None
|
||
download_hash: Optional[str] = None
|
||
torrent_name: Optional[str] = None
|
||
torrent_description: Optional[str] = None
|
||
torrent_site: Optional[str] = None
|
||
userid: Optional[str] = None
|
||
username: Optional[str] = None
|
||
channel: Optional[str] = None
|
||
date: Optional[str] = None
|
||
note: Optional[JsonData] = None
|
||
media_category_id: Optional[str] = None
|
||
media_category: Optional[str] = None
|
||
classification_rule_id: Optional[str] = None
|
||
classification_policy_revision: Optional[int] = None
|
||
classification_source: Optional[str] = None
|
||
episode_group: Optional[str] = None
|
||
custom_words: Optional[str] = None
|
||
|
||
def __post_init__(self) -> None:
|
||
"""
|
||
递归冻结可变 JSON 字段,使 DTO 在所有层级都不可修改。
|
||
"""
|
||
object.__setattr__(self, "note", _freeze_json(self.note))
|
||
|
||
|
||
@dataclass(frozen=True, slots=True)
|
||
class DownloadFileSnapshot:
|
||
"""
|
||
脱离数据库会话的下载文件关联快照。
|
||
|
||
:param id: 下载文件关联记录的数据库主键
|
||
:param downloader: 下载器配置名称
|
||
:param download_hash: 下载器中的任务哈希标识
|
||
:param fullpath: 文件的完整下载路径
|
||
:param savepath: 下载任务的根保存目录
|
||
:param filepath: 下载任务中的文件相对路径
|
||
:param torrentname: 关联种子的原始名称
|
||
:param state: 文件有效状态,1 表示正常,0 表示已删除
|
||
"""
|
||
|
||
id: int
|
||
downloader: Optional[str]
|
||
download_hash: Optional[str]
|
||
fullpath: Optional[str]
|
||
savepath: Optional[str]
|
||
filepath: Optional[str]
|
||
torrentname: Optional[str]
|
||
state: int
|
||
|
||
|
||
@dataclass(frozen=True, slots=True)
|
||
class DownloadHistoryWrite:
|
||
"""
|
||
一次下载成功后写入历史所需的完整稳定数据。
|
||
|
||
:param path: 下载任务的保存路径
|
||
:param type: 媒体类型,区分电影、电视剧和音乐
|
||
:param title: 识别出的标准媒体标题
|
||
:param year: 媒体发行年份;未知时为 None
|
||
:param media_source: 标准媒体来源,与 media_id 配对使用
|
||
:param media_id: 当前来源中的原生媒体标识
|
||
:param music_type: 音乐实体类型,区分单曲录音与专辑
|
||
:param seasons: 已识别的季号或季范围文本
|
||
:param episodes: 识别出的集号或集范围文本
|
||
:param image: 通知和历史展示使用的图片地址
|
||
:param poster: 媒体海报地址
|
||
:param downloader: 下载器配置名称
|
||
:param download_hash: 下载器中的任务哈希标识
|
||
:param torrent_name: 种子原始标题
|
||
:param torrent_description: 种子副标题或简介
|
||
:param torrent_site: 种子所属站点名称
|
||
:param userid: 触发下载的消息渠道用户标识
|
||
:param username: 触发下载的本地用户名
|
||
:param channel: 触发下载的消息渠道名称
|
||
:param date: 历史记录的时间文本
|
||
:param note: 冻结的附加 JSON 信息,包含恢复音乐上下文所需的扩展数据
|
||
:param media_category_id: 媒体类别的稳定标识
|
||
:param media_category: 下载时保存的媒体类别路径快照
|
||
:param classification_rule_id: 当次分类命中的规则标识
|
||
:param classification_policy_revision: 当次执行使用的分类策略版本
|
||
:param classification_source: 当次最终分类的依据来源
|
||
:param episode_group: 当前媒体使用的剧集组标识
|
||
:param custom_words: 该任务专用的自定义识别词
|
||
"""
|
||
|
||
path: str
|
||
type: str
|
||
title: str
|
||
year: Optional[str] = None
|
||
media_source: Optional[MediaSource] = None
|
||
media_id: Optional[str] = None
|
||
music_type: Optional[str] = None
|
||
seasons: Optional[str] = None
|
||
episodes: Optional[str] = None
|
||
image: Optional[str] = None
|
||
poster: Optional[str] = None
|
||
downloader: Optional[str] = None
|
||
download_hash: Optional[str] = None
|
||
torrent_name: Optional[str] = None
|
||
torrent_description: Optional[str] = None
|
||
torrent_site: Optional[str] = None
|
||
userid: Optional[Union[str, int]] = None
|
||
username: Optional[str] = None
|
||
channel: Optional[str] = None
|
||
date: Optional[str] = None
|
||
note: Optional[JsonData] = None
|
||
media_category_id: Optional[str] = None
|
||
media_category: Optional[str] = None
|
||
classification_rule_id: Optional[str] = None
|
||
classification_policy_revision: Optional[int] = None
|
||
classification_source: Optional[str] = None
|
||
episode_group: Optional[str] = None
|
||
custom_words: Optional[str] = None
|
||
|
||
def to_payload(self) -> dict[str, Any]:
|
||
"""
|
||
返回可交给持久化适配器的独立字段副本。
|
||
"""
|
||
payload = asdict(self)
|
||
if self.media_source is not None:
|
||
payload["media_source"] = str(self.media_source)
|
||
if self.userid is not None:
|
||
payload["userid"] = str(self.userid)
|
||
return payload
|
||
|
||
|
||
@dataclass(frozen=True, slots=True)
|
||
class DownloadFileWrite:
|
||
"""
|
||
下载任务关联文件的一次稳定写入。
|
||
|
||
:param downloader: 下载器配置名称
|
||
:param download_hash: 下载器中的任务哈希标识
|
||
:param fullpath: 文件的完整下载路径
|
||
:param savepath: 下载任务的根保存目录
|
||
:param filepath: 下载任务中的文件相对路径
|
||
:param torrentname: 关联种子的原始名称
|
||
:param state: 文件有效状态,1 表示正常,0 表示已删除
|
||
"""
|
||
|
||
downloader: Optional[str] = None
|
||
download_hash: Optional[str] = None
|
||
fullpath: Optional[str] = None
|
||
savepath: Optional[str] = None
|
||
filepath: Optional[str] = None
|
||
torrentname: Optional[str] = None
|
||
state: int = 1
|
||
|
||
def to_payload(self) -> dict[str, Any]:
|
||
"""
|
||
返回可交给持久化适配器的独立字段副本。
|
||
"""
|
||
return asdict(self)
|
||
|
||
|
||
class DownloadHistoryQueryPort(Protocol):
|
||
"""
|
||
宿主下载、订阅、Agent 和整理用例所需的类型化查询端口。
|
||
"""
|
||
|
||
def get_by_hash(
|
||
self,
|
||
download_hash: str,
|
||
) -> Optional[DownloadHistorySnapshot]:
|
||
"""
|
||
按下载任务 Hash 返回最新历史快照。
|
||
|
||
:param download_hash: 下载器中的任务哈希标识
|
||
:return: 该任务最新的下载历史;不存在时返回 None
|
||
"""
|
||
...
|
||
|
||
def get_by_hashes(
|
||
self,
|
||
download_hashes: list[str],
|
||
) -> dict[str, DownloadHistorySnapshot]:
|
||
"""
|
||
批量返回以下载任务 Hash 为键的最新历史快照。
|
||
|
||
:param download_hashes: 待批量查询的下载任务哈希标识
|
||
:return: 以任务哈希为键的最新历史映射;未找到的哈希不写入映射
|
||
"""
|
||
...
|
||
|
||
def get_by_path(self, path: str) -> Optional[DownloadHistorySnapshot]:
|
||
"""
|
||
按下载保存路径返回历史快照。
|
||
|
||
:param path: 下载任务的保存路径
|
||
:return: 匹配保存路径的历史快照;不存在时返回 None
|
||
"""
|
||
...
|
||
|
||
def get_by_media_identity(
|
||
self,
|
||
media_source: MediaSource,
|
||
media_id: str,
|
||
music_type: Optional[str] = None,
|
||
) -> list[DownloadHistorySnapshot]:
|
||
"""
|
||
按规范媒体身份返回历史快照。
|
||
|
||
:param media_source: 标准媒体来源,与 media_id 配对使用
|
||
:param media_id: 当前来源中的原生媒体标识
|
||
:param music_type: 音乐实体类型筛选;None 不附加该条件
|
||
:return: 匹配媒体身份的全部下载历史快照
|
||
"""
|
||
...
|
||
|
||
def get_file_by_fullpath(
|
||
self,
|
||
fullpath: str,
|
||
) -> Optional[DownloadFileSnapshot]:
|
||
"""
|
||
按完整路径返回一条有效下载文件快照。
|
||
|
||
:param fullpath: 下载文件的完整路径
|
||
:return: 该路径的有效下载文件快照;不存在时返回 None
|
||
"""
|
||
...
|
||
|
||
def get_files_by_hash(
|
||
self,
|
||
download_hash: str,
|
||
state: Optional[int] = None,
|
||
) -> list[DownloadFileSnapshot]:
|
||
"""
|
||
按下载任务 Hash 返回文件快照。
|
||
|
||
:param download_hash: 下载器中的任务哈希标识
|
||
:param state: 文件有效状态筛选;None 表示不限状态
|
||
:return: 满足任务哈希及状态筛选的文件快照列表
|
||
"""
|
||
...
|
||
|
||
def get_files_by_savepath(self, savepath: str) -> list[DownloadFileSnapshot]:
|
||
"""
|
||
按保存目录返回下载文件快照。
|
||
|
||
:param savepath: 下载任务的根保存目录
|
||
:return: 该保存目录下的下载文件快照列表
|
||
"""
|
||
...
|
||
|
||
async def async_list_by_page(
|
||
self,
|
||
page: int = 1,
|
||
count: int = 30,
|
||
) -> list[DownloadHistorySnapshot]:
|
||
"""
|
||
异步按下载时间倒序分页返回历史快照。
|
||
|
||
:param page: 页码,从 1 开始
|
||
:param count: 每页最多返回的记录数
|
||
:return: 按下载时间倒序排列的当前页历史快照
|
||
"""
|
||
...
|
||
|
||
|
||
class DownloadHistoryWritePort(Protocol):
|
||
"""
|
||
下载历史新增和删除所需的类型化事务端口。
|
||
"""
|
||
|
||
def add(
|
||
self,
|
||
history: DownloadHistoryWrite,
|
||
files: tuple[DownloadFileWrite, ...] = (),
|
||
) -> int:
|
||
"""
|
||
在单一事务中新增历史与关联文件并返回历史 ID。
|
||
|
||
:param history: 待持久化的完整历史字段快照
|
||
:param files: 与下载任务一起保存的关联文件快照,默认不新增文件
|
||
:return: 已提交的新下载历史主键
|
||
"""
|
||
...
|
||
|
||
async def async_delete(self, history_id: int) -> None:
|
||
"""
|
||
在独立异步事务中删除指定历史。
|
||
|
||
:param history_id: 目标历史记录的数据库主键
|
||
"""
|
||
...
|
||
|
||
|
||
class DownloadHistoryRepository(DownloadHistoryQueryPort, DownloadHistoryWritePort, Protocol):
|
||
"""
|
||
组合宿主所需全部下载历史查询和变更能力。
|
||
"""
|
||
|
||
|
||
class AsyncDownloadHistoryQueryRepository(Protocol):
|
||
"""
|
||
下载历史只读用例需要的最小异步持久化端口。
|
||
"""
|
||
|
||
async def async_list_by_page(self, page: int = 1, count: int = 30) -> list[DownloadHistorySnapshot]:
|
||
"""
|
||
按下载时间倒序分页读取历史记录。
|
||
|
||
:param page: 页码,从 1 开始
|
||
:param count: 每页最多返回的记录数
|
||
:return: 按下载时间倒序排列的当前页历史快照
|
||
"""
|
||
...
|
||
|
||
async def async_count(self) -> int:
|
||
"""
|
||
返回下载历史记录总数。
|
||
|
||
:return: 下载历史记录总数
|
||
"""
|
||
...
|
||
|
||
|
||
@dataclass(frozen=True, slots=True)
|
||
class ManualTransferHistory:
|
||
"""
|
||
手动整理准备阶段需要的稳定历史投影。
|
||
|
||
:param id: 整理历史记录的数据库主键
|
||
:param status: 本次整理是否成功
|
||
:param mode: 整理方式,例如移动、复制或链接
|
||
:param src_fileitem: 源文件项的JSON 快照
|
||
:param dest_fileitem: 目标文件项的JSON 快照
|
||
:param downloader: 下载器配置名称
|
||
:param download_hash: 下载器中的任务哈希标识
|
||
:param type: 媒体类型,区分电影、电视剧和音乐
|
||
:param media_source: 标准媒体来源,与 media_id 配对使用
|
||
:param media_id: 当前来源中的原生媒体标识
|
||
:param music_type: 音乐实体类型,区分单曲录音与专辑
|
||
:param seasons: 已识别的季号或季范围文本
|
||
:param episodes: 识别出的集号或集范围文本
|
||
:param episode_group: 当前媒体使用的剧集组标识
|
||
"""
|
||
|
||
id: int
|
||
status: bool
|
||
mode: Optional[str]
|
||
src_fileitem: Optional[dict[str, JsonData]]
|
||
dest_fileitem: Optional[dict[str, JsonData]]
|
||
downloader: Optional[str]
|
||
download_hash: Optional[str]
|
||
type: Optional[str]
|
||
media_source: Optional[MediaSource]
|
||
media_id: Optional[str]
|
||
music_type: Optional[str]
|
||
seasons: Optional[str]
|
||
episodes: Optional[str]
|
||
episode_group: Optional[str]
|
||
|
||
|
||
class TransferHistoryLookupRepository(Protocol):
|
||
"""
|
||
手动整理历史投影所需的同步查询端口。
|
||
"""
|
||
|
||
def get(self, history_id: int) -> Optional[TransferHistorySnapshot]:
|
||
"""
|
||
按主键读取整理历史。
|
||
|
||
:param history_id: 目标历史记录的数据库主键
|
||
:return: 匹配的整理历史快照;不存在时返回 None
|
||
"""
|
||
...
|
||
|
||
|
||
class TransferHistoryLookupService:
|
||
"""
|
||
向同步整理用例提供脱离 ORM 会话的历史投影。
|
||
"""
|
||
|
||
def __init__(self, repository: TransferHistoryLookupRepository) -> None:
|
||
"""
|
||
保存整理历史只读端口。
|
||
"""
|
||
self._repository = repository
|
||
|
||
def get(self, history_id: int) -> Optional[ManualTransferHistory]:
|
||
"""
|
||
按主键读取手动整理所需字段。
|
||
"""
|
||
record = self._repository.get(history_id)
|
||
if record is None:
|
||
return None
|
||
src_fileitem = (
|
||
record.src_fileitem if isinstance(record.src_fileitem, dict) else None
|
||
)
|
||
dest_fileitem = (
|
||
record.dest_fileitem if isinstance(record.dest_fileitem, dict) else None
|
||
)
|
||
return ManualTransferHistory(
|
||
id=record.id,
|
||
status=bool(record.status),
|
||
mode=record.mode,
|
||
src_fileitem=src_fileitem,
|
||
dest_fileitem=dest_fileitem,
|
||
downloader=record.downloader,
|
||
download_hash=record.download_hash,
|
||
type=record.type,
|
||
media_source=record.media_source,
|
||
media_id=record.media_id,
|
||
music_type=getattr(record, "music_type", None),
|
||
seasons=record.seasons,
|
||
episodes=record.episodes,
|
||
episode_group=record.episode_group,
|
||
)
|
||
|
||
|
||
class HistoryQueryService:
|
||
"""
|
||
提供历史列表和详情 DTO,隔离 API 与数据库模型。
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
*,
|
||
download_repository: AsyncDownloadHistoryQueryRepository,
|
||
transfer_repository: TransferHistoryQueryPort,
|
||
) -> None:
|
||
"""
|
||
保存下载历史和整理历史的只读端口。
|
||
"""
|
||
self._download_repository = download_repository
|
||
self._transfer_repository = transfer_repository
|
||
|
||
async def list_download(
|
||
self,
|
||
*,
|
||
page: int = 1,
|
||
count: int = 30,
|
||
) -> list[DownloadHistory]:
|
||
"""
|
||
分页读取下载历史并转换为稳定的接口 DTO。
|
||
"""
|
||
records = await self._download_repository.async_list_by_page(page, count)
|
||
return [DownloadHistory.model_validate(record) for record in records]
|
||
|
||
async def count_download(self) -> int:
|
||
"""
|
||
返回下载历史精确总数,供分页 API 通过附加元数据报告。
|
||
"""
|
||
return await self._download_repository.async_count()
|
||
|
||
async def list_transfer(
|
||
self,
|
||
*,
|
||
title: Optional[str] = None,
|
||
page: int = 1,
|
||
count: int = 30,
|
||
status: Optional[bool] = None,
|
||
) -> TransferHistoryPage:
|
||
"""
|
||
应用历史筛选规则并返回整理历史分页 DTO。
|
||
"""
|
||
if title:
|
||
wildcard = "*" in title or "?" in title
|
||
if wildcard:
|
||
pattern = self._glob_to_like(title)
|
||
else:
|
||
pattern = "%".join(jieba_cut(title, HMM=False))
|
||
total = await self._transfer_repository.async_count_by_title(
|
||
pattern,
|
||
status=status,
|
||
wildcard=wildcard,
|
||
)
|
||
records = await self._transfer_repository.async_list_by_title(
|
||
pattern,
|
||
page=page,
|
||
count=count,
|
||
status=status,
|
||
wildcard=wildcard,
|
||
)
|
||
else:
|
||
records = await self._transfer_repository.async_list_by_page(
|
||
page=page,
|
||
count=count,
|
||
status=status,
|
||
)
|
||
total = await self._transfer_repository.async_count(status=status)
|
||
|
||
return TransferHistoryPage(
|
||
list=[TransferHistory.model_validate(record) for record in records],
|
||
total=int(total or 0),
|
||
)
|
||
|
||
async def get_transfer(self, history_id: int) -> Optional[TransferHistory]:
|
||
"""
|
||
读取单条整理历史 DTO,不向调用方泄漏 ORM 实例。
|
||
"""
|
||
record = await self._transfer_repository.async_get(history_id)
|
||
if record is None:
|
||
return None
|
||
history: TransferHistory = TransferHistory.model_validate(record)
|
||
return history
|
||
|
||
async def get_transfers(
|
||
self,
|
||
history_ids: list[int],
|
||
) -> tuple[list[TransferHistory], list[int]]:
|
||
"""
|
||
按输入顺序读取多条整理历史,并同时返回缺失 ID。
|
||
"""
|
||
records: list[TransferHistory] = []
|
||
missing_ids: list[int] = []
|
||
for history_id in history_ids:
|
||
record = await self.get_transfer(history_id)
|
||
if record is None:
|
||
missing_ids.append(history_id)
|
||
else:
|
||
records.append(record)
|
||
return records, missing_ids
|
||
|
||
@staticmethod
|
||
def _glob_to_like(pattern: str) -> str:
|
||
"""
|
||
将 glob 通配符转换为使用反斜杠转义的 SQL LIKE 模式。
|
||
"""
|
||
result = pattern.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
||
return result.replace("*", "%").replace("?", "_")
|
||
|
||
|
||
# 整理历史写入口保留仓储事务契约;领域对象的字段映射由 transfer.history 统一维护。
|
||
|
||
def add_transfer_success(
|
||
fileitem: FileItem,
|
||
mode: str,
|
||
meta: MetaBase,
|
||
mediainfo: Union[MediaInfo, MusicInfo],
|
||
transferinfo: TransferInfo,
|
||
downloader: Optional[str] = None,
|
||
download_hash: Optional[str] = None,
|
||
transfer_history_oper: Optional[TransferHistoryReplacePort] = None,
|
||
) -> TransferHistorySnapshot:
|
||
"""
|
||
新增转移成功历史记录。
|
||
:param fileitem: 源文件项
|
||
:param mode: 整理方式
|
||
:param meta: 文件名识别结果
|
||
:param mediainfo: 媒体识别结果
|
||
:param transferinfo: 整理结果
|
||
:param downloader: 下载器
|
||
:param download_hash: 种子 hash
|
||
:param transfer_history_oper: 兼容旧关键字的暂存端口,未传时使用组合根仓储
|
||
:return: 落库后的整理记录
|
||
"""
|
||
repository = transfer_history_oper or get_transfer_history_repository()
|
||
fields = history_projection.success_fields(
|
||
fileitem=fileitem,
|
||
mode=mode,
|
||
meta=meta,
|
||
mediainfo=mediainfo,
|
||
transferinfo=transferinfo,
|
||
downloader=downloader,
|
||
download_hash=download_hash,
|
||
)
|
||
return repository.replace(TransferHistoryWrite(**fields))
|
||
|
||
|
||
def add_transfer_fail(
|
||
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,
|
||
transfer_history_oper: Optional[TransferHistoryReplacePort] = None,
|
||
) -> TransferHistorySnapshot:
|
||
"""
|
||
新增转移失败历史记录。
|
||
|
||
识别结果与整理结果齐备时按完整字段落库;缺任一项则走「未识别到媒体信息」分支,
|
||
此时仅保存已有文件名元数据,不写目标路径;缺少元数据也必须保留源文件与失败原因。
|
||
|
||
: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: 是否已达到自动整理暂停阈值
|
||
:param transfer_history_oper: 兼容旧关键字的暂存端口,未传时使用组合根仓储
|
||
:return: 落库后的整理记录
|
||
"""
|
||
repository = transfer_history_oper or get_transfer_history_repository()
|
||
fields = history_projection.failure_fields(
|
||
fileitem=fileitem,
|
||
mode=mode,
|
||
meta=meta,
|
||
mediainfo=mediainfo,
|
||
transferinfo=transferinfo,
|
||
downloader=downloader,
|
||
download_hash=download_hash,
|
||
retry_count=retry_count,
|
||
auto_paused=auto_paused,
|
||
)
|
||
return repository.replace(TransferHistoryWrite(**fields))
|