Files
MoviePilot/app/schemas/transfer.py
2026-08-13 13:26:17 +08:00

364 lines
11 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.
from pathlib import Path
from typing import Any, Callable, List, Optional, Union
from pydantic import BaseModel, Field
from app.schemas.media import OptionalMediaIdentityMixin
from app.schemas.types import MediaSource, MusicTargetEntityType
from app.schemas.context import MetaInfo, MediaInfo
from app.schemas.music import MusicInfo, MusicMeta
from app.schemas.file import FileItem
from app.schemas.history import DownloadHistory
from app.schemas.system import TransferDirectoryConf
from app.schemas.tmdb import TmdbEpisode
from app.schemas.types import MediaType
class DownloaderTorrent(BaseModel):
"""
下载器任务信息
"""
downloader: Optional[str] = None
hash: Optional[str] = None
title: Optional[str] = None
site_name: Optional[str] = None
name: Optional[str] = None
year: Optional[str] = None
season_episode: Optional[str] = None
path: Optional[Path] = None
size: Optional[float] = 0.0
progress: Optional[float] = 0.0
state: Optional[str] = 'downloading'
upspeed: Optional[str] = None
dlspeed: Optional[str] = None
tags: Optional[str] = None
save_path: Optional[str] = None
content_path: Optional[str] = None
category: Optional[str] = None
download_limit: Optional[float] = None
upload_limit: Optional[float] = None
ratio_limit: Optional[float] = None
seeding_time_limit: Optional[int] = None
trackers: Optional[List[str]] = Field(default_factory=list)
media: Optional["DownloadTaskMedia"] = None
userid: Optional[str] = None
username: Optional[str] = None
left_time: Optional[str] = None
class DownloadTaskMedia(OptionalMediaIdentityMixin, BaseModel):
"""下载任务关联的影视或音乐媒体摘要。"""
type: Optional[str] = None
title: Optional[str] = None
season: Optional[list[int] | int | str] = None
episode: Optional[list[int] | int | str] = None
image: Optional[str] = None
poster: Optional[str] = None
backdrop: Optional[str] = None
media_source: Optional[MediaSource] = None
media_id: Optional[str] = None
music_type: Optional[str] = None
artists: list[str] = Field(default_factory=list)
album: Optional[str] = None
album_id: Optional[str] = None
total_tracks: Optional[int] = None
track_number: Optional[int] = None
class TransferTorrent(DownloaderTorrent):
"""
待转移任务信息
"""
class DownloadingTorrent(DownloaderTorrent):
"""
下载中任务信息
"""
class TransferTask(OptionalMediaIdentityMixin, BaseModel):
"""
文件整理任务
"""
fileitem: FileItem
meta: Optional[Any] = None
mediainfo: Optional[Any] = None
media_source: Optional[MediaSource] = None
media_id: Optional[str] = None
mtype: Optional[MediaType] = None
target_directory: Optional[TransferDirectoryConf] = None
target_storage: Optional[str] = None
target_path: Optional[Path] = None
transfer_type: Optional[str] = None
scrape: Optional[bool] = False
library_type_folder: Optional[bool] = False
library_category_folder: Optional[bool] = False
episodes_info: Optional[List[TmdbEpisode]] = None
username: Optional[str] = None
downloader: Optional[str] = None
download_hash: Optional[str] = None
download_history: Optional[DownloadHistory] = None
transfer_batch_id: Optional[str] = None
manual: Optional[bool] = False
background: Optional[bool] = True
preview: Optional[bool] = False
def to_dict(self):
"""
返回字典
"""
dicts = vars(self).copy()
dicts["fileitem"] = self.fileitem.model_dump() if self.fileitem else None
dicts["meta"] = self.meta.model_dump() if self.meta else None
dicts["mediainfo"] = self.mediainfo.model_dump() if self.mediainfo else None
dicts["target_directory"] = self.target_directory.model_dump() if self.target_directory else None
return dicts
class TransferJobTask(BaseModel):
"""
文件整理作业任务
"""
fileitem: Optional[FileItem] = None
meta: Optional[Union[MusicMeta, MetaInfo]] = None
state: Optional[str] = None
downloader: Optional[str] = None
download_hash: Optional[str] = None
class TransferJob(BaseModel):
"""
文件整理作业
"""
media: Optional[Union[MusicInfo, MediaInfo]] = None
season: Optional[int] = None
tasks: Optional[List[TransferJobTask]] = Field(default_factory=list)
class TransferInfo(BaseModel):
"""
文件整理结果
"""
# 是否成功标志
success: bool = True
# 整理⼁路径
fileitem: Optional[FileItem] = None
# 转移后的目录项,媒体的根目录
target_diritem: Optional[FileItem] = None
# 转移后路径
target_item: Optional[FileItem] = None
# 整理方式
transfer_type: Optional[str] = None
# 处理文件数
file_count: Optional[int] = Field(default=0)
# 处理文件清单
file_list: Optional[list] = Field(default_factory=list)
# 目标文件清单
file_list_new: Optional[list] = Field(default_factory=list)
# 总文件大小
total_size: Optional[int] = Field(default=0)
# 失败清单
fail_list: Optional[list] = Field(default_factory=list)
# 错误信息
message: Optional[str] = None
# 是否需要刮削
need_scrape: Optional[bool] = False
# 是否需要通知
need_notify: Optional[bool] = False
# 是否因覆盖模式判定「不覆盖」而放弃整理。
# 这是一次正常的策略裁决而非整理故障,调用方据此决定是否写失败历史与推送失败通知
overwrite_skipped: Optional[bool] = False
def to_dict(self):
"""
返回字典
"""
dicts = vars(self).copy()
dicts["fileitem"] = self.fileitem.model_dump() if self.fileitem else None
dicts["target_item"] = self.target_item.model_dump() if self.target_item else None
return dicts
class TransferQueue(BaseModel):
"""
异步整理队列信息
"""
# 任务信息
task: Optional[TransferTask] = None
# 回调函数
callback: Optional[Callable] = None
# 整理结果
result: Optional[TransferInfo] = None
class EpisodeFormat(BaseModel):
"""
剧集自定义识别格式
"""
format: Optional[str] = None
detail: Optional[str] = None
part: Optional[str] = None
offset: Optional[str] = None
class EpisodeFormatRule(BaseModel):
"""
集数定位规则
"""
name: str
enabled: bool = True
order: int = 0
pattern: str
min_file_size_mb: int = 0
class EpisodeFormatRecommendItem(BaseModel):
"""
集数定位推荐请求
"""
fileitem: Optional[FileItem] = None
fileitems: Optional[List[FileItem]] = None
class ManualTransferItem(OptionalMediaIdentityMixin, BaseModel):
"""手动整理请求,媒体身份接受内置或插件来源与原生 ID。"""
# 文件项
fileitem: FileItem = None
# 文件项列表(前端多选时传入)
fileitems: Optional[List[FileItem]] = None
# 日志ID
logid: Optional[int] = None
# 日志ID列表前端多选历史记录时传入
logids: Optional[List[int]] = None
# 目标存储
target_storage: Optional[str] = None
# 目标路径
target_path: Optional[str] = None
# 媒体数据源
media_source: Optional[MediaSource] = None
# 数据源原生ID
media_id: Optional[str] = None
# 音乐实体类型
music_type: Optional[MusicTargetEntityType] = None
# 类型
type_name: Optional[str] = None
# 季号
season: Optional[int] = None
# 整理方式
transfer_type: Optional[str] = None
# 自定义格式
episode_format: Optional[str] = None
# 指定集数
episode_detail: Optional[str] = None
# 指定PART
episode_part: Optional[str] = None
# 集数偏移
episode_offset: Optional[str] = None
# 最小文件大小
min_filesize: Optional[int] = 0
# 刮削
scrape: Optional[bool] = False
# 媒体库类型子目录
library_type_folder: Optional[bool] = None
# 媒体库类别子目录
library_category_folder: Optional[bool] = None
# 复用历史识别信息
from_history: Optional[bool] = False
# 剧集组
episode_group: Optional[str] = None
# 仅预览,不执行整理
preview: Optional[bool] = False
# 重新整理,清理命中的成功历史及其旧目标
reorganize: Optional[bool] = False
class ManualTransferHistoryInfo(BaseModel):
"""
手动整理命中的成功历史摘要
"""
# 是否应显示重新整理操作
reorganize: bool = False
# 命中的成功历史数量
history_count: int = 0
class ManualTransferTargetPath(BaseModel):
"""
手动整理目的路径匹配结果
"""
# 目标存储
target_storage: Optional[str] = None
# 目标路径
target_path: Optional[str] = None
# 整理方式
transfer_type: Optional[str] = None
# 刮削
scrape: Optional[bool] = False
# 媒体库类型子目录
library_type_folder: Optional[bool] = False
# 媒体库类别子目录
library_category_folder: Optional[bool] = False
class ManualTransferPreviewSummary(BaseModel):
"""手动整理预览数量统计。"""
total: int = 0
success: int = 0
failed: int = 0
class ManualTransferPreviewItem(BaseModel):
"""单个文件的手动整理预览。"""
source: Optional[str] = None
target: Optional[str] = None
target_dir: Optional[str] = None
success: bool = False
message: Optional[str] = None
type: Optional[str] = None
title: Optional[str] = None
season: Optional[int] = None
episode: Optional[int] = None
episode_end: Optional[int] = None
part: Optional[int | str] = None
org_string: Optional[str] = None
apply_words: list[str] = Field(default_factory=list)
resource_team: Optional[str] = None
customization: Optional[str] = None
class ManualTransferResultData(BaseModel):
"""手动整理预览或执行结果数据。"""
summary: Optional[ManualTransferPreviewSummary] = None
items: list[ManualTransferPreviewItem] = Field(default_factory=list)
message: Optional[str] = None
class EpisodeFormatRecommendData(BaseModel):
"""集数定位模板推荐结果。"""
rule_name: str
episode_format: str
sample_file: str
pattern: Optional[str] = None
rule_index: Optional[int] = None
min_file_size_mb: Optional[int] = None
sample_count: Optional[int] = None
majority_count: Optional[int] = None
confidence: Optional[str] = None
size_filter_relaxed: Optional[bool] = None
native_verified_count: Optional[int] = None
native_fallback_count: Optional[int] = None
native_conflict_count: Optional[int] = None
reason: Optional[str] = None
reasons: list[str] = Field(default_factory=list)
message: Optional[str] = None