mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 07:27:15 +08:00
fix transfer
This commit is contained in:
@@ -17,11 +17,9 @@ from app.db.models.downloadhistory import DownloadHistory
|
|||||||
from app.db.models.transferhistory import TransferHistory
|
from app.db.models.transferhistory import TransferHistory
|
||||||
from app.db.systemconfig_oper import SystemConfigOper
|
from app.db.systemconfig_oper import SystemConfigOper
|
||||||
from app.db.transferhistory_oper import TransferHistoryOper
|
from app.db.transferhistory_oper import TransferHistoryOper
|
||||||
from app.modules.filetransfer.storage.alipan import AliyunHelper
|
|
||||||
from app.helper.directory import DirectoryHelper
|
from app.helper.directory import DirectoryHelper
|
||||||
from app.helper.format import FormatParser
|
from app.helper.format import FormatParser
|
||||||
from app.helper.progress import ProgressHelper
|
from app.helper.progress import ProgressHelper
|
||||||
from app.modules.filetransfer.storage.u115 import U115Helper
|
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.schemas import TransferInfo, TransferTorrent, Notification, EpisodeFormat
|
from app.schemas import TransferInfo, TransferTorrent, Notification, EpisodeFormat
|
||||||
from app.schemas.types import TorrentStatus, EventType, MediaType, ProgressKey, NotificationType, MessageChannel, \
|
from app.schemas.types import TorrentStatus, EventType, MediaType, ProgressKey, NotificationType, MessageChannel, \
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import importlib
|
|||||||
import pkgutil
|
import pkgutil
|
||||||
import traceback
|
import traceback
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import List, Any
|
||||||
|
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ class ModuleHelper:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, package_path: str, filter_func=lambda name, obj: True):
|
def load(cls, package_path: str, filter_func=lambda name, obj: True) -> List[Any]:
|
||||||
"""
|
"""
|
||||||
导入模块
|
导入模块
|
||||||
:param package_path: 父包名
|
:param package_path: 父包名
|
||||||
@@ -41,7 +42,7 @@ class ModuleHelper:
|
|||||||
return submodules
|
return submodules
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load_with_pre_filter(cls, package_path: str, filter_func=lambda name, obj: True):
|
def load_with_pre_filter(cls, package_path: str, filter_func=lambda name, obj: True) -> List[Any]:
|
||||||
"""
|
"""
|
||||||
导入子模块
|
导入子模块
|
||||||
:param package_path: 父包名
|
:param package_path: 父包名
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ from app.core.meta import MetaBase
|
|||||||
from app.core.metainfo import MetaInfo, MetaInfoPath
|
from app.core.metainfo import MetaInfo, MetaInfoPath
|
||||||
from app.helper.directory import DirectoryHelper
|
from app.helper.directory import DirectoryHelper
|
||||||
from app.helper.message import MessageHelper
|
from app.helper.message import MessageHelper
|
||||||
|
from app.helper.module import ModuleHelper
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.modules import _ModuleBase
|
from app.modules import _ModuleBase
|
||||||
from app.schemas import TransferInfo, ExistMediaInfo, TmdbEpisode, MediaDirectory
|
from app.schemas import TransferInfo, ExistMediaInfo, TmdbEpisode, MediaDirectory, FileItem
|
||||||
from app.schemas.types import MediaType
|
from app.schemas.types import MediaType
|
||||||
from app.utils.system import SystemUtils
|
from app.utils.system import SystemUtils
|
||||||
|
|
||||||
@@ -25,13 +26,17 @@ class FileTransferModule(_ModuleBase):
|
|||||||
文件整理模块
|
文件整理模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_storage_schemas = []
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.directoryhelper = DirectoryHelper()
|
self.directoryhelper = DirectoryHelper()
|
||||||
self.messagehelper = MessageHelper()
|
self.messagehelper = MessageHelper()
|
||||||
|
|
||||||
def init_module(self) -> None:
|
def init_module(self) -> None:
|
||||||
pass
|
# 加载模块
|
||||||
|
self._storage_schemas = ModuleHelper.load('app.modules.filetransfer.storage',
|
||||||
|
filter_func=lambda _, obj: hasattr(obj, 'schema'))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_name() -> str:
|
def get_name() -> str:
|
||||||
@@ -102,33 +107,34 @@ class FileTransferModule(_ModuleBase):
|
|||||||
)
|
)
|
||||||
return str(path)
|
return str(path)
|
||||||
|
|
||||||
def transfer(self, path: Path, meta: MetaBase, mediainfo: MediaInfo,
|
def transfer(self, fileitem: FileItem, meta: MetaBase, mediainfo: MediaInfo,
|
||||||
transfer_type: str, target: Path = None,
|
transfer_type: str, target_storage: str = None, target_path: Path = None,
|
||||||
episodes_info: List[TmdbEpisode] = None,
|
episodes_info: List[TmdbEpisode] = None,
|
||||||
scrape: bool = None) -> TransferInfo:
|
scrape: bool = None) -> TransferInfo:
|
||||||
"""
|
"""
|
||||||
文件转移
|
文件整理
|
||||||
:param path: 文件路径
|
:param fileitem: 源文件
|
||||||
:param meta: 预识别的元数据,仅单文件转移时传递
|
:param meta: 预识别的元数据,仅单文件整理时传递
|
||||||
:param mediainfo: 识别的媒体信息
|
:param mediainfo: 识别的媒体信息
|
||||||
:param transfer_type: 转移方式
|
:param transfer_type: 整理方式
|
||||||
:param target: 目标路径
|
:param target_storage: 目标存储
|
||||||
|
:param target_path: 目标路径
|
||||||
:param episodes_info: 当前季的全部集信息
|
:param episodes_info: 当前季的全部集信息
|
||||||
:param scrape: 是否刮削元数据
|
:param scrape: 是否刮削元数据
|
||||||
:return: {path, target_path, message}
|
:return: {path, target_path, message}
|
||||||
"""
|
"""
|
||||||
# 目标路径不能是文件
|
# 目标路径不能是文件
|
||||||
if target and target.is_file():
|
if target_path and target_path.is_file():
|
||||||
logger.error(f"转移目标路径是一个文件 {target} 是一个文件")
|
logger.error(f"整理目标路径 {target_path} 是一个文件")
|
||||||
return TransferInfo(success=False,
|
return TransferInfo(success=False,
|
||||||
path=path,
|
path=fileitem.path,
|
||||||
message=f"{target} 不是有效目录")
|
message=f"{target_path} 不是有效目录")
|
||||||
# 获取目标路径
|
# 获取目标路径
|
||||||
directoryhelper = DirectoryHelper()
|
directoryhelper = DirectoryHelper()
|
||||||
if target:
|
if target_path:
|
||||||
dir_info = directoryhelper.get_library_dir(mediainfo, in_path=path, to_path=target)
|
dir_info = directoryhelper.get_library_dir(mediainfo, in_path=fileitem.path, to_path=target_path)
|
||||||
else:
|
else:
|
||||||
dir_info = directoryhelper.get_library_dir(mediainfo, in_path=path)
|
dir_info = directoryhelper.get_library_dir(mediainfo, in_path=fileitem.path)
|
||||||
if dir_info:
|
if dir_info:
|
||||||
# 是否需要刮削
|
# 是否需要刮削
|
||||||
if scrape is None:
|
if scrape is None:
|
||||||
@@ -136,86 +142,111 @@ class FileTransferModule(_ModuleBase):
|
|||||||
else:
|
else:
|
||||||
need_scrape = scrape
|
need_scrape = scrape
|
||||||
# 拼装媒体库一、二级子目录
|
# 拼装媒体库一、二级子目录
|
||||||
target = self.__get_dest_dir(mediainfo=mediainfo, target_dir=dir_info)
|
target_path = self.__get_dest_dir(mediainfo=mediainfo, target_dir=dir_info)
|
||||||
elif target:
|
elif target_path:
|
||||||
# 自定义目标路径
|
# 自定义目标路径
|
||||||
need_scrape = scrape or False
|
need_scrape = scrape or False
|
||||||
else:
|
else:
|
||||||
# 未找到有效的媒体库目录
|
# 未找到有效的媒体库目录
|
||||||
logger.error(
|
logger.error(
|
||||||
f"{mediainfo.type.value} {mediainfo.title_year} 未找到有效的媒体库目录,无法转移文件,源路径:{path}")
|
f"{mediainfo.type.value} {mediainfo.title_year} 未找到有效的媒体库目录,无法整理文件,源路径:{fileitem.path}")
|
||||||
return TransferInfo(success=False,
|
return TransferInfo(success=False,
|
||||||
path=path,
|
path=fileitem.path,
|
||||||
message="未找到有效的媒体库目录")
|
message="未找到有效的媒体库目录")
|
||||||
|
|
||||||
logger.info(f"获取转移目标路径:{target}")
|
logger.info(f"获取整理目标路径:{target_path}")
|
||||||
# 转移
|
# 整理
|
||||||
return self.transfer_media(in_path=path,
|
return self.transfer_media(fileitem=fileitem,
|
||||||
in_meta=meta,
|
in_meta=meta,
|
||||||
mediainfo=mediainfo,
|
mediainfo=mediainfo,
|
||||||
transfer_type=transfer_type,
|
transfer_type=transfer_type,
|
||||||
target_dir=target,
|
target_storage=target_storage,
|
||||||
|
target_path=target_path,
|
||||||
episodes_info=episodes_info,
|
episodes_info=episodes_info,
|
||||||
need_scrape=need_scrape)
|
need_scrape=need_scrape)
|
||||||
|
|
||||||
@staticmethod
|
def __get_storage_oper(self, storage: str):
|
||||||
def __transfer_command(file_item: Path, target_file: Path, transfer_type: str) -> int:
|
"""
|
||||||
|
获取存储操作对象
|
||||||
|
"""
|
||||||
|
for storage_schema in self._storage_schemas:
|
||||||
|
if storage_schema.schema == storage:
|
||||||
|
return storage_schema()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __list_files(self, fileitem: FileItem):
|
||||||
|
"""
|
||||||
|
浏览文件
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __transfer_command(self, fileitem: FileItem, target_storage: str,
|
||||||
|
target_file: Path, transfer_type: str) -> int:
|
||||||
"""
|
"""
|
||||||
使用系统命令处理单个文件
|
使用系统命令处理单个文件
|
||||||
:param file_item: 文件路径
|
:param fileitem: 源文件
|
||||||
|
:param target_storage: 目标存储
|
||||||
:param target_file: 目标文件路径
|
:param target_file: 目标文件路径
|
||||||
:param transfer_type: RmtMode转移方式
|
:param transfer_type: 整理方式
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if fileitem.storage != "local" and target_storage != "local":
|
||||||
|
logger.error(f"不支持 {fileitem.storage} 到 {target_storage} 的文件整理")
|
||||||
|
return 1
|
||||||
|
retcode = 0
|
||||||
|
# 源操作对象
|
||||||
|
source_oper = self.__get_storage_oper(fileitem.storage)
|
||||||
|
# 目的操作对象
|
||||||
|
target_oper = self.__get_storage_oper(target_storage)
|
||||||
|
|
||||||
with lock:
|
with lock:
|
||||||
|
if fileitem.storage == "local" and target_storage == "local":
|
||||||
|
# 本地到本地
|
||||||
|
if transfer_type == "copy":
|
||||||
|
retcode = source_oper.copy(fileitem, target_file)
|
||||||
|
elif transfer_type == "move":
|
||||||
|
retcode = source_oper.move(fileitem, target_file)
|
||||||
|
elif transfer_type == "link":
|
||||||
|
retcode = source_oper.link(fileitem, target_file)
|
||||||
|
elif transfer_type == "softlink":
|
||||||
|
retcode = source_oper.softlink(fileitem, target_file)
|
||||||
|
# TODO 本地到网盘
|
||||||
|
|
||||||
# 转移
|
# TODO 网盘到本地
|
||||||
if transfer_type == 'link':
|
|
||||||
# 硬链接
|
|
||||||
retcode, retmsg = SystemUtils.link(file_item, target_file)
|
|
||||||
elif transfer_type == 'softlink':
|
|
||||||
# 软链接
|
|
||||||
retcode, retmsg = SystemUtils.softlink(file_item, target_file)
|
|
||||||
elif transfer_type == 'move':
|
|
||||||
# 移动
|
|
||||||
retcode, retmsg = SystemUtils.move(file_item, target_file)
|
|
||||||
elif transfer_type == 'rclone_move':
|
|
||||||
# Rclone 移动
|
|
||||||
retcode, retmsg = SystemUtils.rclone_move(file_item, target_file)
|
|
||||||
elif transfer_type == 'rclone_copy':
|
|
||||||
# Rclone 复制
|
|
||||||
retcode, retmsg = SystemUtils.rclone_copy(file_item, target_file)
|
|
||||||
else:
|
|
||||||
# 复制
|
|
||||||
retcode, retmsg = SystemUtils.copy(file_item, target_file)
|
|
||||||
|
|
||||||
if retcode != 0:
|
|
||||||
logger.error(retmsg)
|
|
||||||
|
|
||||||
return retcode
|
return retcode
|
||||||
|
|
||||||
def __transfer_other_files(self, org_path: Path, new_path: Path,
|
def __transfer_other_files(self, fileitem: FileItem, target_storage: str, target_file: Path,
|
||||||
transfer_type: str, over_flag: bool) -> int:
|
transfer_type: str) -> int:
|
||||||
"""
|
"""
|
||||||
根据文件名转移其他相关文件
|
根据文件名整理其他相关文件
|
||||||
:param org_path: 原文件名
|
:param fileitem: 源文件
|
||||||
:param new_path: 新文件名
|
:param target_storage: 目标存储
|
||||||
:param transfer_type: RmtMode转移方式
|
:param target_file: 目标路径
|
||||||
:param over_flag: 是否覆盖,为True时会先删除再转移
|
:param transfer_type: 整理方式
|
||||||
"""
|
"""
|
||||||
retcode = self.__transfer_subtitles(org_path, new_path, transfer_type)
|
retcode = self.__transfer_subtitles(fileitem=fileitem,
|
||||||
|
target_storage=target_storage,
|
||||||
|
target_file=target_file,
|
||||||
|
transfer_type=transfer_type)
|
||||||
if retcode != 0:
|
if retcode != 0:
|
||||||
return retcode
|
return retcode
|
||||||
retcode = self.__transfer_audio_track_files(org_path, new_path, transfer_type, over_flag)
|
retcode = self.__transfer_audio_track_files(fileitem=fileitem,
|
||||||
|
target_storage=target_storage,
|
||||||
|
target_file=target_file,
|
||||||
|
transfer_type=transfer_type)
|
||||||
if retcode != 0:
|
if retcode != 0:
|
||||||
return retcode
|
return retcode
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def __transfer_subtitles(self, org_path: Path, new_path: Path, transfer_type: str) -> int:
|
def __transfer_subtitles(self, fileitem: FileItem, target_storage: str, target_file: Path,
|
||||||
|
transfer_type: str) -> int:
|
||||||
"""
|
"""
|
||||||
根据文件名转移对应字幕文件
|
根据文件名整理对应字幕文件
|
||||||
:param org_path: 原文件名
|
:param fileitem: 源文件
|
||||||
:param new_path: 新文件名
|
:param target_storage: 目标存储
|
||||||
:param transfer_type: RmtMode转移方式
|
:param target_file: 目标路径
|
||||||
|
:param transfer_type: 整理方式
|
||||||
"""
|
"""
|
||||||
# 字幕正则式
|
# 字幕正则式
|
||||||
_zhcn_sub_re = r"([.\[(](((zh[-_])?(cn|ch[si]|sg|sc))|zho?" \
|
_zhcn_sub_re = r"([.\[(](((zh[-_])?(cn|ch[si]|sg|sc))|zho?" \
|
||||||
@@ -230,26 +261,34 @@ class FileTransferModule(_ModuleBase):
|
|||||||
r"|(?<![a-z0-9])big5(?![a-z0-9])"
|
r"|(?<![a-z0-9])big5(?![a-z0-9])"
|
||||||
_eng_sub_re = r"[.\[(]eng[.\])]"
|
_eng_sub_re = r"[.\[(]eng[.\])]"
|
||||||
|
|
||||||
# 比对文件名并转移字幕
|
# 比对文件名并整理字幕
|
||||||
|
org_path = Path(fileitem.path)
|
||||||
org_dir: Path = org_path.parent
|
org_dir: Path = org_path.parent
|
||||||
file_list: List[Path] = SystemUtils.list_files(org_dir, settings.RMT_SUBEXT)
|
# 列出所有字幕文件
|
||||||
|
storage_oper = self.__get_storage_oper(fileitem.storage)
|
||||||
|
if not storage_oper:
|
||||||
|
logger.error(f"不支持 {fileitem.storage} 的文件整理")
|
||||||
|
return 1
|
||||||
|
file_list: List[FileItem] = storage_oper.list(fileitem)
|
||||||
if len(file_list) == 0:
|
if len(file_list) == 0:
|
||||||
logger.debug(f"{org_dir} 目录下没有找到字幕文件...")
|
logger.debug(f"{org_dir} 目录下没有找到字幕文件...")
|
||||||
else:
|
else:
|
||||||
logger.debug("字幕文件清单:" + str(file_list))
|
logger.debug("字幕文件清单:" + str(file_list))
|
||||||
# 识别文件名
|
# 识别文件名
|
||||||
metainfo = MetaInfo(title=org_path.name)
|
metainfo = MetaInfoPath(org_path)
|
||||||
for file_item in file_list:
|
for sub_item in file_list:
|
||||||
|
if f".{sub_item.extension.lower()}" not in settings.RMT_SUBEXT:
|
||||||
|
continue
|
||||||
# 识别字幕文件名
|
# 识别字幕文件名
|
||||||
sub_file_name = re.sub(_zhtw_sub_re,
|
sub_file_name = re.sub(_zhtw_sub_re,
|
||||||
".",
|
".",
|
||||||
re.sub(_zhcn_sub_re,
|
re.sub(_zhcn_sub_re,
|
||||||
".",
|
".",
|
||||||
file_item.name,
|
sub_item.name,
|
||||||
flags=re.I),
|
flags=re.I),
|
||||||
flags=re.I)
|
flags=re.I)
|
||||||
sub_file_name = re.sub(_eng_sub_re, ".", sub_file_name, flags=re.I)
|
sub_file_name = re.sub(_eng_sub_re, ".", sub_file_name, flags=re.I)
|
||||||
sub_metainfo = MetaInfo(title=file_item.name)
|
sub_metainfo = MetaInfoPath(Path(sub_item.path))
|
||||||
# 匹配字幕文件名
|
# 匹配字幕文件名
|
||||||
if (org_path.stem == Path(sub_file_name).stem) or \
|
if (org_path.stem == Path(sub_file_name).stem) or \
|
||||||
(sub_metainfo.cn_name and sub_metainfo.cn_name == metainfo.cn_name) \
|
(sub_metainfo.cn_name and sub_metainfo.cn_name == metainfo.cn_name) \
|
||||||
@@ -264,15 +303,15 @@ class FileTransferModule(_ModuleBase):
|
|||||||
continue
|
continue
|
||||||
new_file_type = ""
|
new_file_type = ""
|
||||||
# 兼容jellyfin字幕识别(多重识别), emby则会识别最后一个后缀
|
# 兼容jellyfin字幕识别(多重识别), emby则会识别最后一个后缀
|
||||||
if re.search(_zhcn_sub_re, file_item.name, re.I):
|
if re.search(_zhcn_sub_re, sub_item.name, re.I):
|
||||||
new_file_type = ".chi.zh-cn"
|
new_file_type = ".chi.zh-cn"
|
||||||
elif re.search(_zhtw_sub_re, file_item.name,
|
elif re.search(_zhtw_sub_re, sub_item.name,
|
||||||
re.I):
|
re.I):
|
||||||
new_file_type = ".zh-tw"
|
new_file_type = ".zh-tw"
|
||||||
elif re.search(_eng_sub_re, file_item.name, re.I):
|
elif re.search(_eng_sub_re, sub_item.name, re.I):
|
||||||
new_file_type = ".eng"
|
new_file_type = ".eng"
|
||||||
# 通过对比字幕文件大小 尽量转移所有存在的字幕
|
# 通过对比字幕文件大小 尽量整理所有存在的字幕
|
||||||
file_ext = file_item.suffix
|
file_ext = f".{sub_item.extension}"
|
||||||
new_sub_tag_dict = {
|
new_sub_tag_dict = {
|
||||||
".eng": ".英文",
|
".eng": ".英文",
|
||||||
".chi.zh-cn": ".简体中文",
|
".chi.zh-cn": ".简体中文",
|
||||||
@@ -286,147 +325,158 @@ class FileTransferModule(_ModuleBase):
|
|||||||
t) for t in range(6)
|
t) for t in range(6)
|
||||||
]
|
]
|
||||||
for new_sub_tag in new_sub_tag_list:
|
for new_sub_tag in new_sub_tag_list:
|
||||||
new_file: Path = new_path.with_name(new_path.stem + new_sub_tag + file_ext)
|
new_file: Path = target_file.with_name(target_file.stem + new_sub_tag + file_ext)
|
||||||
# 如果字幕文件不存在, 直接转移字幕, 并跳出循环
|
# 如果字幕文件不存在, 直接整理字幕, 并跳出循环
|
||||||
try:
|
try:
|
||||||
if not new_file.exists():
|
logger.debug(f"正在处理字幕:{sub_item.name}")
|
||||||
logger.debug(f"正在处理字幕:{file_item.name}")
|
retcode = self.__transfer_command(fileitem=sub_item,
|
||||||
retcode = self.__transfer_command(file_item=file_item,
|
target_storage=target_storage,
|
||||||
target_file=new_file,
|
target_file=new_file,
|
||||||
transfer_type=transfer_type)
|
transfer_type=transfer_type)
|
||||||
if retcode == 0:
|
if retcode == 0:
|
||||||
logger.info(f"字幕 {file_item.name} {transfer_type}完成")
|
logger.info(f"字幕 {sub_item.name} {transfer_type}完成")
|
||||||
break
|
|
||||||
else:
|
|
||||||
logger.error(f"字幕 {file_item.name} {transfer_type}失败,错误码 {retcode}")
|
|
||||||
return retcode
|
|
||||||
# 如果字幕文件的大小与已存在文件相同, 说明已经转移过了, 则跳出循环
|
|
||||||
elif new_file.stat().st_size == file_item.stat().st_size:
|
|
||||||
logger.info(f"字幕 new_file 已存在")
|
|
||||||
break
|
break
|
||||||
# 否则 循环继续 > 通过new_sub_tag_list 获取新的tag附加到字幕文件名, 继续检查是否能转移
|
else:
|
||||||
except OSError as reason:
|
logger.error(f"字幕 {sub_item.name} {transfer_type}失败,错误码 {retcode}")
|
||||||
logger.info(f"字幕 {new_file} 出错了,原因: {reason}")
|
return retcode
|
||||||
|
except Exception as error:
|
||||||
|
logger.info(f"字幕 {new_file} 出错了,原因: {str(error)}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def __transfer_audio_track_files(self, org_path: Path, new_path: Path,
|
def __transfer_audio_track_files(self, fileitem: FileItem, target_storage: str, target_file: Path,
|
||||||
transfer_type: str, over_flag: bool) -> int:
|
transfer_type: str) -> int:
|
||||||
"""
|
"""
|
||||||
根据文件名转移对应音轨文件
|
根据文件名整理对应音轨文件
|
||||||
:param org_path: 原文件名
|
:param fileitem: 源文件
|
||||||
:param new_path: 新文件名
|
:param target_storage: 目标存储
|
||||||
:param transfer_type: RmtMode转移方式
|
:param target_file: 目标路径
|
||||||
:param over_flag: 是否覆盖,为True时会先删除再转移
|
:param transfer_type: 整理方式
|
||||||
"""
|
"""
|
||||||
|
org_path = Path(fileitem.path)
|
||||||
dir_name = org_path.parent
|
dir_name = org_path.parent
|
||||||
file_name = org_path.name
|
file_name = org_path.name
|
||||||
file_list: List[Path] = SystemUtils.list_files(dir_name, ['.mka'])
|
# 列出所有音轨文件
|
||||||
pending_file_list: List[Path] = [file for file in file_list if org_path.stem == file.stem]
|
storage_oper = self.__get_storage_oper(fileitem.storage)
|
||||||
|
if not storage_oper:
|
||||||
|
logger.error(f"不支持 {fileitem.storage} 的文件整理")
|
||||||
|
return 1
|
||||||
|
file_list: List[FileItem] = storage_oper.list(fileitem)
|
||||||
|
# 匹配音轨文件
|
||||||
|
pending_file_list: List[FileItem] = [file for file in file_list if Path(file.name).stem == org_path.name
|
||||||
|
and f".{file.extension.lower()}" in settings.RMT_AUDIOEXT]
|
||||||
if len(pending_file_list) == 0:
|
if len(pending_file_list) == 0:
|
||||||
logger.debug(f"{dir_name} 目录下没有找到匹配的音轨文件")
|
logger.debug(f"{dir_name} 目录下没有找到匹配的音轨文件")
|
||||||
else:
|
return 0
|
||||||
logger.debug("音轨文件清单:" + str(pending_file_list))
|
logger.debug("音轨文件清单:" + str(pending_file_list))
|
||||||
for track_file in pending_file_list:
|
for track_file in pending_file_list:
|
||||||
track_ext = track_file.suffix
|
track_ext = f".{track_file.extension}"
|
||||||
new_track_file = new_path.with_name(new_path.stem + track_ext)
|
new_track_file = target_file.with_name(target_file.stem + track_ext)
|
||||||
if new_track_file.exists():
|
try:
|
||||||
if not over_flag:
|
logger.info(f"正在整理音轨文件:{track_file} 到 {new_track_file}")
|
||||||
logger.warn(f"音轨文件已存在:{new_track_file}")
|
retcode = self.__transfer_command(fileitem=track_file,
|
||||||
continue
|
target_storage=target_storage,
|
||||||
else:
|
target_file=new_track_file,
|
||||||
logger.info(f"正在删除已存在的音轨文件:{new_track_file}")
|
transfer_type=transfer_type)
|
||||||
new_track_file.unlink()
|
if retcode == 0:
|
||||||
try:
|
logger.info(f"音轨文件 {file_name} {transfer_type}完成")
|
||||||
logger.info(f"正在转移音轨文件:{track_file} 到 {new_track_file}")
|
else:
|
||||||
retcode = self.__transfer_command(file_item=track_file,
|
logger.error(f"音轨文件 {file_name} {transfer_type}失败,错误码:{retcode}")
|
||||||
target_file=new_track_file,
|
except Exception as error:
|
||||||
transfer_type=transfer_type)
|
logger.error(f"音轨文件 {file_name} {transfer_type}失败:{str(error)}")
|
||||||
if retcode == 0:
|
|
||||||
logger.info(f"音轨文件 {file_name} {transfer_type}完成")
|
|
||||||
else:
|
|
||||||
logger.error(f"音轨文件 {file_name} {transfer_type}失败,错误码:{retcode}")
|
|
||||||
except OSError as reason:
|
|
||||||
logger.error(f"音轨文件 {file_name} {transfer_type}失败:{reason}")
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def __transfer_dir(self, file_path: Path, new_path: Path, transfer_type: str) -> int:
|
def __transfer_dir(self, fileitem: FileItem, transfer_type: str,
|
||||||
|
target_storage: str, target_path: Path) -> int:
|
||||||
"""
|
"""
|
||||||
转移整个文件夹
|
整理整个文件夹
|
||||||
:param file_path: 原路径
|
:param fileitem: 源文件
|
||||||
:param new_path: 新路径
|
:param transfer_type: 整理方式
|
||||||
:param transfer_type: RmtMode转移方式
|
:param target_storage: 目标存储
|
||||||
|
:param target_path: 目标路径
|
||||||
"""
|
"""
|
||||||
logger.info(f"正在{transfer_type}目录:{file_path} 到 {new_path}")
|
logger.info(f"正在{transfer_type}目录:{fileitem.path} 到 {target_path}")
|
||||||
# 复制
|
# 复制
|
||||||
retcode = self.__transfer_dir_files(src_dir=file_path,
|
retcode = self.__transfer_dir_files(fileitem=fileitem,
|
||||||
target_dir=new_path,
|
target_storage=target_storage,
|
||||||
|
target_path=target_path,
|
||||||
transfer_type=transfer_type)
|
transfer_type=transfer_type)
|
||||||
if retcode == 0:
|
if retcode == 0:
|
||||||
logger.info(f"文件 {file_path} {transfer_type}完成")
|
logger.info(f"文件 {fileitem.path} {transfer_type}完成")
|
||||||
else:
|
else:
|
||||||
logger.error(f"文件{file_path} {transfer_type}失败,错误码:{retcode}")
|
logger.error(f"文件{fileitem.path} {transfer_type}失败,错误码:{retcode}")
|
||||||
|
|
||||||
return retcode
|
return retcode
|
||||||
|
|
||||||
def __transfer_dir_files(self, src_dir: Path, target_dir: Path, transfer_type: str) -> int:
|
def __transfer_dir_files(self, fileitem: FileItem, transfer_type: str,
|
||||||
|
target_storage: str, target_path: Path) -> int:
|
||||||
"""
|
"""
|
||||||
按目录结构转移目录下所有文件
|
按目录结构整理目录下所有文件
|
||||||
:param src_dir: 原路径
|
:param fileitem: 源文件
|
||||||
:param target_dir: 新路径
|
:param target_storage: 目标存储
|
||||||
:param transfer_type: RmtMode转移方式
|
:param target_path: 目标路径
|
||||||
|
:param transfer_type: 整理方式
|
||||||
"""
|
"""
|
||||||
retcode = 0
|
retcode = 0
|
||||||
for file in src_dir.glob("**/*"):
|
# 列出所有文件
|
||||||
# 过滤掉目录
|
storage_oper = self.__get_storage_oper(fileitem.storage)
|
||||||
if file.is_dir():
|
if not storage_oper:
|
||||||
continue
|
logger.error(f"不支持 {fileitem.storage} 的文件整理")
|
||||||
# 使用target_dir的父目录作为新的父目录
|
return 1
|
||||||
new_file = target_dir.joinpath(file.relative_to(src_dir))
|
file_list: List[FileItem] = storage_oper.list(fileitem)
|
||||||
if new_file.exists():
|
# 整理文件
|
||||||
logger.warn(f"{new_file} 文件已存在")
|
for item in file_list:
|
||||||
continue
|
if item.type == "dir":
|
||||||
if not new_file.parent.exists():
|
# 递归整理目录
|
||||||
new_file.parent.mkdir(parents=True, exist_ok=True)
|
new_path = target_path / item.name
|
||||||
retcode = self.__transfer_command(file_item=file,
|
retcode = self.__transfer_dir(fileitem=item,
|
||||||
target_file=new_file,
|
transfer_type=transfer_type,
|
||||||
transfer_type=transfer_type)
|
target_storage=target_storage,
|
||||||
if retcode != 0:
|
target_path=new_path)
|
||||||
break
|
if retcode != 0:
|
||||||
|
return retcode
|
||||||
|
else:
|
||||||
|
# 整理文件
|
||||||
|
new_file = target_path / item.name
|
||||||
|
retcode = self.__transfer_command(fileitem=item,
|
||||||
|
target_storage=target_storage,
|
||||||
|
target_file=new_file,
|
||||||
|
transfer_type=transfer_type)
|
||||||
|
if retcode != 0:
|
||||||
|
return retcode
|
||||||
return retcode
|
return retcode
|
||||||
|
|
||||||
def __transfer_file(self, file_item: Path, new_file: Path, transfer_type: str,
|
def __transfer_file(self, fileitem: FileItem, target_storage: str, target_file: Path,
|
||||||
over_flag: bool = False) -> int:
|
transfer_type: str, over_flag: bool = False) -> int:
|
||||||
"""
|
"""
|
||||||
转移一个文件,同时处理其他相关文件
|
整理一个文件,同时处理其他相关文件
|
||||||
:param file_item: 原文件路径
|
:param fileitem: 原文件
|
||||||
:param new_file: 新文件路径
|
:param target_storage: 目标存储
|
||||||
:param transfer_type: RmtMode转移方式
|
:param target_file: 新文件
|
||||||
:param over_flag: 是否覆盖,为True时会先删除再转移
|
:param transfer_type: 整理方式
|
||||||
|
:param over_flag: 是否覆盖,为True时会先删除再整理
|
||||||
"""
|
"""
|
||||||
if new_file.exists() or new_file.is_symlink():
|
if target_storage == "local" and (target_file.exists() or target_file.is_symlink()):
|
||||||
if not over_flag:
|
if not over_flag:
|
||||||
logger.warn(f"文件已存在:{new_file}")
|
logger.warn(f"文件已存在:{target_file}")
|
||||||
return 0
|
return 0
|
||||||
else:
|
else:
|
||||||
logger.info(f"正在删除已存在的文件:{new_file}")
|
logger.info(f"正在删除已存在的文件:{target_file}")
|
||||||
new_file.unlink()
|
target_file.unlink()
|
||||||
logger.info(f"正在转移文件:{file_item} 到 {new_file}")
|
logger.info(f"正在整理文件:{fileitem.path} 到 {target_file}")
|
||||||
# 创建父目录
|
retcode = self.__transfer_command(fileitem=fileitem,
|
||||||
new_file.parent.mkdir(parents=True, exist_ok=True)
|
target_storage=target_storage,
|
||||||
retcode = self.__transfer_command(file_item=file_item,
|
target_file=target_file,
|
||||||
target_file=new_file,
|
|
||||||
transfer_type=transfer_type)
|
transfer_type=transfer_type)
|
||||||
if retcode == 0:
|
if retcode == 0:
|
||||||
logger.info(f"文件 {file_item} {transfer_type}完成")
|
logger.info(f"文件 {fileitem.path} {transfer_type}完成")
|
||||||
else:
|
else:
|
||||||
logger.error(f"文件 {file_item} {transfer_type}失败,错误码:{retcode}")
|
logger.error(f"文件 {fileitem.path} {transfer_type}失败,错误码:{retcode}")
|
||||||
return retcode
|
return retcode
|
||||||
# 处理其他相关文件
|
# 处理其他相关文件
|
||||||
return self.__transfer_other_files(org_path=file_item,
|
return self.__transfer_other_files(fileitem=fileitem,
|
||||||
new_path=new_file,
|
target_storage=target_storage,
|
||||||
transfer_type=transfer_type,
|
target_file=target_file,
|
||||||
over_flag=over_flag)
|
transfer_type=transfer_type)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __get_dest_dir(mediainfo: MediaInfo, target_dir: MediaDirectory) -> Path:
|
def __get_dest_dir(mediainfo: MediaInfo, target_dir: MediaDirectory) -> Path:
|
||||||
@@ -449,87 +499,82 @@ class FileTransferModule(_ModuleBase):
|
|||||||
return download_dir
|
return download_dir
|
||||||
|
|
||||||
def transfer_media(self,
|
def transfer_media(self,
|
||||||
in_path: Path,
|
fileitem: FileItem,
|
||||||
in_meta: MetaBase,
|
in_meta: MetaBase,
|
||||||
mediainfo: MediaInfo,
|
mediainfo: MediaInfo,
|
||||||
transfer_type: str,
|
transfer_type: str,
|
||||||
target_dir: Path,
|
target_storage: str,
|
||||||
|
target_path: Path,
|
||||||
episodes_info: List[TmdbEpisode] = None,
|
episodes_info: List[TmdbEpisode] = None,
|
||||||
need_scrape: bool = False
|
need_scrape: bool = False
|
||||||
) -> TransferInfo:
|
) -> TransferInfo:
|
||||||
"""
|
"""
|
||||||
识别并转移一个文件或者一个目录下的所有文件
|
识别并整理一个文件或者一个目录下的所有文件
|
||||||
:param in_path: 转移的路径,可能是一个文件也可以是一个目录
|
:param fileitem: 整理的文件对象,可能是一个文件也可以是一个目录
|
||||||
:param in_meta:预识别元数据
|
:param in_meta:预识别元数据
|
||||||
:param mediainfo: 媒体信息
|
:param mediainfo: 媒体信息
|
||||||
:param target_dir: 媒体库根目录
|
:param target_storage: 目标存储
|
||||||
:param transfer_type: 文件转移方式
|
:param target_path: 目标路径
|
||||||
|
:param transfer_type: 文件整理方式
|
||||||
:param episodes_info: 当前季的全部集信息
|
:param episodes_info: 当前季的全部集信息
|
||||||
:param need_scrape: 是否需要刮削
|
:param need_scrape: 是否需要刮削
|
||||||
:return: TransferInfo、错误信息
|
:return: TransferInfo、错误信息
|
||||||
"""
|
"""
|
||||||
# 检查目录路径
|
# 检查目录路径
|
||||||
if not in_path.exists():
|
|
||||||
return TransferInfo(success=False,
|
|
||||||
path=in_path,
|
|
||||||
message=f"{in_path} 路径不存在")
|
|
||||||
|
|
||||||
if transfer_type not in ['rclone_copy', 'rclone_move']:
|
if fileitem.storage == "local" and not Path(fileitem.path).exists():
|
||||||
|
return TransferInfo(success=False,
|
||||||
|
path=fileitem.path,
|
||||||
|
message=f"{fileitem.path} 不存在")
|
||||||
|
|
||||||
|
if target_storage == "local":
|
||||||
# 检查目标路径
|
# 检查目标路径
|
||||||
if not target_dir.exists():
|
if not target_path.exists():
|
||||||
logger.info(f"目标路径不存在,正在创建:{target_dir} ...")
|
logger.info(f"目标路径不存在,正在创建:{target_path} ...")
|
||||||
target_dir.mkdir(parents=True, exist_ok=True)
|
target_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# 重命名格式
|
# 重命名格式
|
||||||
rename_format = settings.TV_RENAME_FORMAT \
|
rename_format = settings.TV_RENAME_FORMAT \
|
||||||
if mediainfo.type == MediaType.TV else settings.MOVIE_RENAME_FORMAT
|
if mediainfo.type == MediaType.TV else settings.MOVIE_RENAME_FORMAT
|
||||||
|
|
||||||
# 判断是否为文件夹
|
# 判断是否为文件夹
|
||||||
if in_path.is_dir():
|
if fileitem.type == "dir":
|
||||||
# 转移整个目录
|
# 整理整个目录,一般为蓝光原盘
|
||||||
# 是否蓝光原盘
|
|
||||||
bluray_flag = SystemUtils.is_bluray_dir(in_path)
|
|
||||||
if bluray_flag:
|
|
||||||
logger.info(f"{in_path} 是蓝光原盘文件夹")
|
|
||||||
# 原文件大小
|
|
||||||
file_size = in_path.stat().st_size
|
|
||||||
# 目的路径
|
|
||||||
new_path = self.get_rename_path(
|
new_path = self.get_rename_path(
|
||||||
path=target_dir,
|
path=target_path,
|
||||||
template_string=rename_format,
|
template_string=rename_format,
|
||||||
rename_dict=self.__get_naming_dict(meta=in_meta,
|
rename_dict=self.__get_naming_dict(meta=in_meta,
|
||||||
mediainfo=mediainfo)
|
mediainfo=mediainfo)
|
||||||
).parent
|
).parent
|
||||||
# 转移蓝光原盘
|
# 整理目录
|
||||||
retcode = self.__transfer_dir(file_path=in_path,
|
retcode = self.__transfer_dir(fileitem=fileitem,
|
||||||
new_path=new_path,
|
target_storage=target_storage,
|
||||||
|
target_path=new_path,
|
||||||
transfer_type=transfer_type)
|
transfer_type=transfer_type)
|
||||||
if retcode != 0:
|
if retcode != 0:
|
||||||
logger.error(f"文件夹 {in_path} 转移失败,错误码:{retcode}")
|
logger.error(f"文件夹 {fileitem.path} 整理失败,错误码:{retcode}")
|
||||||
return TransferInfo(success=False,
|
return TransferInfo(success=False,
|
||||||
message=f"错误码:{retcode}",
|
message=f"错误码:{retcode}",
|
||||||
path=in_path,
|
path=fileitem.path,
|
||||||
target_path=new_path,
|
target_path=new_path)
|
||||||
is_bluray=bluray_flag)
|
|
||||||
|
|
||||||
logger.info(f"文件夹 {in_path} 转移成功")
|
logger.info(f"文件夹 {fileitem.path} 整理成功")
|
||||||
# 返回转移后的路径
|
# 返回整理后的路径
|
||||||
return TransferInfo(success=True,
|
return TransferInfo(success=True,
|
||||||
path=in_path,
|
path=fileitem.path,
|
||||||
target_path=new_path,
|
target_path=new_path,
|
||||||
total_size=file_size,
|
total_size=fileitem.size,
|
||||||
is_bluray=bluray_flag,
|
|
||||||
need_scrape=need_scrape)
|
need_scrape=need_scrape)
|
||||||
else:
|
else:
|
||||||
# 转移单个文件
|
# 整理单个文件
|
||||||
if mediainfo.type == MediaType.TV:
|
if mediainfo.type == MediaType.TV:
|
||||||
# 电视剧
|
# 电视剧
|
||||||
if in_meta.begin_episode is None:
|
if in_meta.begin_episode is None:
|
||||||
logger.warn(f"文件 {in_path} 转移失败:未识别到文件集数")
|
logger.warn(f"文件 {fileitem.path} 整理失败:未识别到文件集数")
|
||||||
return TransferInfo(success=False,
|
return TransferInfo(success=False,
|
||||||
message=f"未识别到文件集数",
|
message=f"未识别到文件集数",
|
||||||
path=in_path,
|
path=fileitem.path,
|
||||||
fail_list=[str(in_path)])
|
fail_list=[fileitem.path])
|
||||||
|
|
||||||
# 文件结束季为空
|
# 文件结束季为空
|
||||||
in_meta.end_season = None
|
in_meta.end_season = None
|
||||||
@@ -543,49 +588,50 @@ class FileTransferModule(_ModuleBase):
|
|||||||
|
|
||||||
# 目的文件名
|
# 目的文件名
|
||||||
new_file = self.get_rename_path(
|
new_file = self.get_rename_path(
|
||||||
path=target_dir,
|
path=target_path,
|
||||||
template_string=rename_format,
|
template_string=rename_format,
|
||||||
rename_dict=self.__get_naming_dict(
|
rename_dict=self.__get_naming_dict(
|
||||||
meta=in_meta,
|
meta=in_meta,
|
||||||
mediainfo=mediainfo,
|
mediainfo=mediainfo,
|
||||||
episodes_info=episodes_info,
|
episodes_info=episodes_info,
|
||||||
file_ext=in_path.suffix
|
file_ext=f".{fileitem.extension}"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 判断是否要覆盖
|
# 判断是否要覆盖
|
||||||
overflag = False
|
overflag = False
|
||||||
target_file = new_file
|
target_file = new_file
|
||||||
if new_file.exists() or new_file.is_symlink():
|
if target_storage == "local" \
|
||||||
|
and (new_file.exists() or new_file.is_symlink()):
|
||||||
if new_file.is_symlink():
|
if new_file.is_symlink():
|
||||||
target_file = new_file.readlink()
|
target_file = new_file.readlink()
|
||||||
if not target_file.exists():
|
if not target_file.exists():
|
||||||
overflag = True
|
overflag = True
|
||||||
if not overflag:
|
if not overflag:
|
||||||
# 目标文件已存在
|
# 目标文件已存在
|
||||||
logger.info(f"目标文件已存在,转移覆盖模式:{settings.OVERWRITE_MODE}")
|
logger.info(f"目标文件已存在,整理覆盖模式:{settings.OVERWRITE_MODE}")
|
||||||
match settings.OVERWRITE_MODE:
|
match settings.OVERWRITE_MODE:
|
||||||
case 'always':
|
case 'always':
|
||||||
# 总是覆盖同名文件
|
# 总是覆盖同名文件
|
||||||
overflag = True
|
overflag = True
|
||||||
case 'size':
|
case 'size':
|
||||||
# 存在时大覆盖小
|
# 存在时大覆盖小
|
||||||
if target_file.stat().st_size < in_path.stat().st_size:
|
if target_file.stat().st_size < fileitem.size:
|
||||||
logger.info(f"目标文件文件大小更小,将覆盖:{new_file}")
|
logger.info(f"目标文件文件大小更小,将覆盖:{new_file}")
|
||||||
overflag = True
|
overflag = True
|
||||||
else:
|
else:
|
||||||
return TransferInfo(success=False,
|
return TransferInfo(success=False,
|
||||||
message=f"媒体库中已存在,且质量更好",
|
message=f"媒体库中已存在,且质量更好",
|
||||||
path=in_path,
|
path=fileitem.path,
|
||||||
target_path=new_file,
|
target_path=new_file,
|
||||||
fail_list=[str(in_path)])
|
fail_list=[fileitem.path])
|
||||||
case 'never':
|
case 'never':
|
||||||
# 存在不覆盖
|
# 存在不覆盖
|
||||||
return TransferInfo(success=False,
|
return TransferInfo(success=False,
|
||||||
message=f"媒体库中已存在,当前设置为不覆盖",
|
message=f"媒体库中已存在,当前设置为不覆盖",
|
||||||
path=in_path,
|
path=fileitem.path,
|
||||||
target_path=new_file,
|
target_path=new_file,
|
||||||
fail_list=[str(in_path)])
|
fail_list=[fileitem.path])
|
||||||
case 'latest':
|
case 'latest':
|
||||||
# 仅保留最新版本
|
# 仅保留最新版本
|
||||||
logger.info(f"仅保留最新版本,将覆盖:{new_file}")
|
logger.info(f"仅保留最新版本,将覆盖:{new_file}")
|
||||||
@@ -593,31 +639,30 @@ class FileTransferModule(_ModuleBase):
|
|||||||
else:
|
else:
|
||||||
if settings.OVERWRITE_MODE == 'latest':
|
if settings.OVERWRITE_MODE == 'latest':
|
||||||
# 文件不存在,但仅保留最新版本
|
# 文件不存在,但仅保留最新版本
|
||||||
logger.info(f"转移覆盖模式:{settings.OVERWRITE_MODE},仅保留最新版本")
|
logger.info(f"整理覆盖模式:{settings.OVERWRITE_MODE},仅保留最新版本")
|
||||||
self.delete_all_version_files(new_file)
|
self.delete_all_version_files(new_file)
|
||||||
# 原文件大小
|
# 整理文件
|
||||||
file_size = in_path.stat().st_size
|
retcode = self.__transfer_file(fileitem=fileitem,
|
||||||
# 转移文件
|
target_storage=target_storage,
|
||||||
retcode = self.__transfer_file(file_item=in_path,
|
target_file=new_file,
|
||||||
new_file=new_file,
|
|
||||||
transfer_type=transfer_type,
|
transfer_type=transfer_type,
|
||||||
over_flag=overflag)
|
over_flag=overflag)
|
||||||
if retcode != 0:
|
if retcode != 0:
|
||||||
logger.error(f"文件 {in_path} 转移失败,错误码:{retcode}")
|
logger.error(f"文件 {fileitem.path} 整理失败,错误码:{retcode}")
|
||||||
return TransferInfo(success=False,
|
return TransferInfo(success=False,
|
||||||
message=f"错误码:{retcode}",
|
message=f"错误码:{retcode}",
|
||||||
path=in_path,
|
path=fileitem.path,
|
||||||
target_path=new_file,
|
target_path=new_file,
|
||||||
fail_list=[str(in_path)])
|
fail_list=[fileitem.path])
|
||||||
|
|
||||||
logger.info(f"文件 {in_path} 转移成功")
|
logger.info(f"文件 {fileitem.path} 整理成功")
|
||||||
return TransferInfo(success=True,
|
return TransferInfo(success=True,
|
||||||
path=in_path,
|
path=fileitem.path,
|
||||||
target_path=new_file,
|
target_path=new_file,
|
||||||
file_count=1,
|
file_count=1,
|
||||||
total_size=file_size,
|
total_size=fileitem.size,
|
||||||
is_bluray=False,
|
is_bluray=False,
|
||||||
file_list=[str(in_path)],
|
file_list=[fileitem.path],
|
||||||
file_list_new=[str(new_file)],
|
file_list_new=[str(new_file)],
|
||||||
need_scrape=need_scrape)
|
need_scrape=need_scrape)
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,21 @@ class StorageBase(metaclass=ABCMeta):
|
|||||||
"""
|
"""
|
||||||
存储基类
|
存储基类
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
transtype = {}
|
||||||
|
|
||||||
|
def support_transtype(self) -> dict:
|
||||||
|
"""
|
||||||
|
支持的整理方式
|
||||||
|
"""
|
||||||
|
return self.transtype
|
||||||
|
|
||||||
|
def is_support_transtype(self, transtype: str) -> bool:
|
||||||
|
"""
|
||||||
|
是否支持整理方式
|
||||||
|
"""
|
||||||
|
return transtype in self.transtype
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def check(self) -> bool:
|
def check(self) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -52,13 +66,6 @@ class StorageBase(metaclass=ABCMeta):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def move(self, fileitm: schemas.FileItem, target_dir: schemas.FileItem) -> bool:
|
|
||||||
"""
|
|
||||||
移动文件
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def upload(self, fileitm: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]:
|
def upload(self, fileitm: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]:
|
||||||
"""
|
"""
|
||||||
@@ -72,4 +79,32 @@ class StorageBase(metaclass=ABCMeta):
|
|||||||
获取文件详情
|
获取文件详情
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def copy(self, fileitm: schemas.FileItem, target_file: Path) -> bool:
|
||||||
|
"""
|
||||||
|
复制文件
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def move(self, fileitm: schemas.FileItem, target_file: Path) -> bool:
|
||||||
|
"""
|
||||||
|
移动文件
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def link(self, fileitm: schemas.FileItem, target_file: Path) -> bool:
|
||||||
|
"""
|
||||||
|
硬链接文件
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def softlink(self, fileitm: schemas.FileItem, target_file: schemas.FileItem) -> bool:
|
||||||
|
"""
|
||||||
|
软链接文件
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ from app.core.config import settings
|
|||||||
from app.db.systemconfig_oper import SystemConfigOper
|
from app.db.systemconfig_oper import SystemConfigOper
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.modules.filetransfer.storage import StorageBase
|
from app.modules.filetransfer.storage import StorageBase
|
||||||
from app.schemas.types import SystemConfigKey
|
from app.schemas.types import SystemConfigKey, StorageSchema
|
||||||
from app.utils.http import RequestUtils
|
from app.utils.http import RequestUtils
|
||||||
from app.utils.string import StringUtils
|
from app.utils.string import StringUtils
|
||||||
from app.utils.system import SystemUtils
|
from app.utils.system import SystemUtils
|
||||||
@@ -23,6 +23,13 @@ class AliPan(StorageBase):
|
|||||||
阿里云相关操作
|
阿里云相关操作
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# 存储类型
|
||||||
|
schema = StorageSchema.Alipan
|
||||||
|
# 支持的整理方式
|
||||||
|
transtype = {
|
||||||
|
"move": "移动"
|
||||||
|
}
|
||||||
|
|
||||||
_X_SIGNATURE = ('f4b7bed5d8524a04051bd2da876dd79afe922b8205226d65855d02b267422adb1'
|
_X_SIGNATURE = ('f4b7bed5d8524a04051bd2da876dd79afe922b8205226d65855d02b267422adb1'
|
||||||
'e0d8a816b021eaf5c36d101892180f79df655c5712b348c2a540ca136e6b22001')
|
'e0d8a816b021eaf5c36d101892180f79df655c5712b348c2a540ca136e6b22001')
|
||||||
|
|
||||||
@@ -531,26 +538,6 @@ class AliPan(StorageBase):
|
|||||||
self.__handle_error(res, "获取下载链接")
|
self.__handle_error(res, "获取下载链接")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def move(self, fileitem: schemas.FileItem, target_dir: schemas.FileItem) -> bool:
|
|
||||||
"""
|
|
||||||
移动文件
|
|
||||||
"""
|
|
||||||
params = self.__access_params
|
|
||||||
if not params:
|
|
||||||
return False
|
|
||||||
headers = self.__get_headers(params)
|
|
||||||
res = RequestUtils(headers=headers, timeout=10).post_res(self.move_file_url, json={
|
|
||||||
"drive_id": fileitem.drive_id,
|
|
||||||
"file_id": fileitem.fileid,
|
|
||||||
"to_parent_file_id": target_dir.fileid,
|
|
||||||
"check_name_mode": "refuse"
|
|
||||||
})
|
|
||||||
if res:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
self.__handle_error(res, "移动文件")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def upload(self, fileitem: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]:
|
def upload(self, fileitem: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]:
|
||||||
"""
|
"""
|
||||||
上传文件,并标记完成
|
上传文件,并标记完成
|
||||||
@@ -625,3 +612,32 @@ class AliPan(StorageBase):
|
|||||||
else:
|
else:
|
||||||
logger.warn("上传文件失败:无法获取上传地址!")
|
logger.warn("上传文件失败:无法获取上传地址!")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def move(self, fileitem: schemas.FileItem, target_dir: schemas.FileItem) -> bool:
|
||||||
|
"""
|
||||||
|
移动文件
|
||||||
|
"""
|
||||||
|
params = self.__access_params
|
||||||
|
if not params:
|
||||||
|
return False
|
||||||
|
headers = self.__get_headers(params)
|
||||||
|
res = RequestUtils(headers=headers, timeout=10).post_res(self.move_file_url, json={
|
||||||
|
"drive_id": fileitem.drive_id,
|
||||||
|
"file_id": fileitem.fileid,
|
||||||
|
"to_parent_file_id": target_dir.fileid,
|
||||||
|
"check_name_mode": "refuse"
|
||||||
|
})
|
||||||
|
if res:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.__handle_error(res, "移动文件")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def copy(self, fileitm: schemas.FileItem, target_file: Path) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def link(self, fileitm: schemas.FileItem, target_file: Path) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def softlink(self, fileitm: schemas.FileItem, target_file: schemas.FileItem) -> bool:
|
||||||
|
pass
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from starlette.responses import FileResponse, Response
|
|||||||
from app import schemas
|
from app import schemas
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.modules.filetransfer.storage import StorageBase
|
from app.modules.filetransfer.storage import StorageBase
|
||||||
|
from app.schemas.types import StorageSchema
|
||||||
from app.utils.system import SystemUtils
|
from app.utils.system import SystemUtils
|
||||||
|
|
||||||
|
|
||||||
@@ -15,6 +16,16 @@ class LocalStorage(StorageBase):
|
|||||||
本地文件操作
|
本地文件操作
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# 存储类型
|
||||||
|
schema = StorageSchema.Local
|
||||||
|
# 支持的整理方式
|
||||||
|
transtype = {
|
||||||
|
"copy": "复制",
|
||||||
|
"move": "移动",
|
||||||
|
"link": "硬链接",
|
||||||
|
"softlink": "软链接"
|
||||||
|
}
|
||||||
|
|
||||||
def check(self) -> bool:
|
def check(self) -> bool:
|
||||||
"""
|
"""
|
||||||
检查存储是否可用
|
检查存储是否可用
|
||||||
@@ -96,9 +107,8 @@ class LocalStorage(StorageBase):
|
|||||||
if not fileitem.path:
|
if not fileitem.path:
|
||||||
return None
|
return None
|
||||||
path_obj = Path(fileitem.path) / name
|
path_obj = Path(fileitem.path) / name
|
||||||
if path_obj.exists():
|
if not path_obj.exists():
|
||||||
return None
|
path_obj.mkdir(parents=True, exist_ok=True)
|
||||||
path_obj.mkdir(parents=True, exist_ok=True)
|
|
||||||
return schemas.FileItem(
|
return schemas.FileItem(
|
||||||
type="dir",
|
type="dir",
|
||||||
path=str(path_obj).replace("\\", "/") + "/",
|
path=str(path_obj).replace("\\", "/") + "/",
|
||||||
@@ -166,19 +176,6 @@ class LocalStorage(StorageBase):
|
|||||||
Path(f"{path_obj.stem}.zip").unlink()
|
Path(f"{path_obj.stem}.zip").unlink()
|
||||||
return reponse
|
return reponse
|
||||||
|
|
||||||
def move(self, fileitem: schemas.FileItem, target_dir: schemas.FileItem) -> bool:
|
|
||||||
"""
|
|
||||||
移动文件
|
|
||||||
"""
|
|
||||||
if not fileitem.path or not target_dir.path:
|
|
||||||
return False
|
|
||||||
path_obj = Path(fileitem.path)
|
|
||||||
target_obj = Path(target_dir.path)
|
|
||||||
if not path_obj.exists() or not target_obj.exists():
|
|
||||||
return False
|
|
||||||
path_obj.rename(target_obj / path_obj.name)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def upload(self, fileitem: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]:
|
def upload(self, fileitem: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]:
|
||||||
"""
|
"""
|
||||||
上传文件
|
上传文件
|
||||||
@@ -198,3 +195,47 @@ class LocalStorage(StorageBase):
|
|||||||
size=path.stat().st_size,
|
size=path.stat().st_size,
|
||||||
modify_time=path.stat().st_mtime,
|
modify_time=path.stat().st_mtime,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def copy(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
|
||||||
|
"""
|
||||||
|
复制文件
|
||||||
|
"""
|
||||||
|
file_path = Path(fileitem.path)
|
||||||
|
code, message = SystemUtils.copy(file_path, target_file)
|
||||||
|
if code != 0:
|
||||||
|
logger.error(f"复制文件失败:{message}")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def link(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
|
||||||
|
"""
|
||||||
|
硬链接文件
|
||||||
|
"""
|
||||||
|
file_path = Path(fileitem.path)
|
||||||
|
code, message = SystemUtils.link(file_path, target_file)
|
||||||
|
if code != 0:
|
||||||
|
logger.error(f"硬链接文件失败:{message}")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def softlink(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
|
||||||
|
"""
|
||||||
|
软链接文件
|
||||||
|
"""
|
||||||
|
file_path = Path(fileitem.path)
|
||||||
|
code, message = SystemUtils.copy(file_path, target_file)
|
||||||
|
if code != 0:
|
||||||
|
logger.error(f"软链接文件失败:{message}")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def move(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
|
||||||
|
"""
|
||||||
|
移动文件
|
||||||
|
"""
|
||||||
|
file_path = Path(fileitem.path)
|
||||||
|
code, message = SystemUtils.move(file_path, target_file)
|
||||||
|
if code != 0:
|
||||||
|
logger.error(f"移动文件失败:{message}")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Any, List
|
||||||
|
|
||||||
|
from app import schemas
|
||||||
|
from app.modules.filetransfer.storage import StorageBase
|
||||||
|
from app.schemas.types import StorageSchema
|
||||||
|
|
||||||
|
|
||||||
|
class Rclone(StorageBase):
|
||||||
|
"""
|
||||||
|
rclone相关操作
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 存储类型
|
||||||
|
schema = StorageSchema.Rclone
|
||||||
|
# 支持的整理方式
|
||||||
|
transtype = {
|
||||||
|
"move": "移动",
|
||||||
|
"copy": "复制"
|
||||||
|
}
|
||||||
|
|
||||||
|
def check(self) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def list(self, fileitm: schemas.FileItem) -> Optional[List[schemas.FileItem]]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def create_folder(self, fileitm: schemas.FileItem, name: str) -> Optional[schemas.FileItem]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def delete(self, fileitm: schemas.FileItem) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def rename(self, fileitm: schemas.FileItem, name: str) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def download(self, fileitm: schemas.FileItem) -> Any:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def upload(self, fileitm: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def detail(self, fileitm: schemas.FileItem) -> Optional[schemas.FileItem]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def move(self, fileitm: schemas.FileItem, target_dir: schemas.FileItem) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def copy(self, fileitm: schemas.FileItem, target_file: Path) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def link(self, fileitm: schemas.FileItem, target_file: Path) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def softlink(self, fileitm: schemas.FileItem, target_file: schemas.FileItem) -> bool:
|
||||||
|
pass
|
||||||
@@ -11,7 +11,7 @@ from app import schemas
|
|||||||
from app.db.systemconfig_oper import SystemConfigOper
|
from app.db.systemconfig_oper import SystemConfigOper
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.modules.filetransfer.storage import StorageBase
|
from app.modules.filetransfer.storage import StorageBase
|
||||||
from app.schemas.types import SystemConfigKey
|
from app.schemas.types import SystemConfigKey, StorageSchema
|
||||||
from app.utils.singleton import Singleton
|
from app.utils.singleton import Singleton
|
||||||
|
|
||||||
|
|
||||||
@@ -20,6 +20,13 @@ class U115Pan(StorageBase, metaclass=Singleton):
|
|||||||
115相关操作
|
115相关操作
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# 存储类型
|
||||||
|
schema = StorageSchema.U115
|
||||||
|
# 支持的整理方式
|
||||||
|
transtype = {
|
||||||
|
"move": "移动"
|
||||||
|
}
|
||||||
|
|
||||||
cloud: Optional[Cloud] = None
|
cloud: Optional[Cloud] = None
|
||||||
_session: QrcodeSession = None
|
_session: QrcodeSession = None
|
||||||
|
|
||||||
@@ -233,19 +240,6 @@ class U115Pan(StorageBase, metaclass=Singleton):
|
|||||||
logger.error(f"115下载失败:{str(e)}")
|
logger.error(f"115下载失败:{str(e)}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def move(self, fileitem: schemas.FileItem, target_dir: schemas.FileItem) -> bool:
|
|
||||||
"""
|
|
||||||
移动文件
|
|
||||||
"""
|
|
||||||
if not self.__init_cloud():
|
|
||||||
return False
|
|
||||||
try:
|
|
||||||
self.cloud.storage().move(fileitem.fileid, target_dir.fileid)
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"移动115文件失败:{str(e)}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def upload(self, fileitem: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]:
|
def upload(self, fileitem: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]:
|
||||||
"""
|
"""
|
||||||
上传文件
|
上传文件
|
||||||
@@ -292,3 +286,25 @@ class U115Pan(StorageBase, metaclass=Singleton):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"上传115文件失败:{str(e)}")
|
logger.error(f"上传115文件失败:{str(e)}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def move(self, fileitem: schemas.FileItem, target_dir: schemas.FileItem) -> bool:
|
||||||
|
"""
|
||||||
|
移动文件
|
||||||
|
"""
|
||||||
|
if not self.__init_cloud():
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
self.cloud.storage().move(fileitem.fileid, target_dir.fileid)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"移动115文件失败:{str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def copy(self, fileitm: schemas.FileItem, target_file: Path) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def link(self, fileitm: schemas.FileItem, target_file: Path) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def softlink(self, fileitm: schemas.FileItem, target_file: schemas.FileItem) -> bool:
|
||||||
|
pass
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ class IndexerModule(_ModuleBase):
|
|||||||
seeding_size=site_obj.seeding_size,
|
seeding_size=site_obj.seeding_size,
|
||||||
seeding_info=site_obj.seeding_info,
|
seeding_info=site_obj.seeding_info,
|
||||||
leeching=site_obj.leeching,
|
leeching=site_obj.leeching,
|
||||||
|
leeching_size=site_obj.leeching_size,
|
||||||
message_unread=site_obj.message_unread,
|
message_unread=site_obj.message_unread,
|
||||||
message_unread_contents=site_obj.message_unread_contents,
|
message_unread_contents=site_obj.message_unread_contents,
|
||||||
updated_at=datetime.now().strftime('%Y-%m-%d'),
|
updated_at=datetime.now().strftime('%Y-%m-%d'),
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ from pydantic import BaseModel
|
|||||||
|
|
||||||
|
|
||||||
class FileItem(BaseModel):
|
class FileItem(BaseModel):
|
||||||
|
# 存储类型
|
||||||
|
storage: Optional[str] = "local"
|
||||||
# 类型 dir/file
|
# 类型 dir/file
|
||||||
type: Optional[str] = None
|
type: Optional[str] = None
|
||||||
# 文件路径
|
# 文件路径
|
||||||
|
|||||||
@@ -42,12 +42,10 @@ class TransferInfo(BaseModel):
|
|||||||
"""
|
"""
|
||||||
# 是否成功标志
|
# 是否成功标志
|
||||||
success: bool = True
|
success: bool = True
|
||||||
# 转移⼁路径
|
# 整理⼁路径
|
||||||
path: Optional[Path] = None
|
path: Optional[Path] = None
|
||||||
# 转移后路径
|
# 转移后路径
|
||||||
target_path: Optional[Path] = None
|
target_path: Optional[Path] = None
|
||||||
# 是否蓝光原盘
|
|
||||||
is_bluray: Optional[bool] = False
|
|
||||||
# 处理文件数
|
# 处理文件数
|
||||||
file_count: Optional[int] = 0
|
file_count: Optional[int] = 0
|
||||||
# 处理文件清单
|
# 处理文件清单
|
||||||
|
|||||||
+13
-1
@@ -1,12 +1,14 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
# 媒体类型
|
||||||
class MediaType(Enum):
|
class MediaType(Enum):
|
||||||
MOVIE = '电影'
|
MOVIE = '电影'
|
||||||
TV = '电视剧'
|
TV = '电视剧'
|
||||||
UNKNOWN = '未知'
|
UNKNOWN = '未知'
|
||||||
|
|
||||||
|
|
||||||
|
# 种子状态
|
||||||
class TorrentStatus(Enum):
|
class TorrentStatus(Enum):
|
||||||
TRANSFER = "可转移"
|
TRANSFER = "可转移"
|
||||||
DOWNLOADING = "下载中"
|
DOWNLOADING = "下载中"
|
||||||
@@ -104,7 +106,7 @@ class SystemConfigKey(Enum):
|
|||||||
class ProgressKey(Enum):
|
class ProgressKey(Enum):
|
||||||
# 搜索
|
# 搜索
|
||||||
Search = "search"
|
Search = "search"
|
||||||
# 转移
|
# 整理
|
||||||
FileTransfer = "filetransfer"
|
FileTransfer = "filetransfer"
|
||||||
# 批量重命名
|
# 批量重命名
|
||||||
BatchRename = "batchrename"
|
BatchRename = "batchrename"
|
||||||
@@ -134,6 +136,7 @@ class NotificationType(Enum):
|
|||||||
Plugin = "插件消息"
|
Plugin = "插件消息"
|
||||||
|
|
||||||
|
|
||||||
|
# 消息渠道
|
||||||
class MessageChannel(Enum):
|
class MessageChannel(Enum):
|
||||||
"""
|
"""
|
||||||
消息渠道
|
消息渠道
|
||||||
@@ -151,3 +154,12 @@ class MessageChannel(Enum):
|
|||||||
class UserConfigKey(Enum):
|
class UserConfigKey(Enum):
|
||||||
# 监控面板
|
# 监控面板
|
||||||
Dashboard = "Dashboard"
|
Dashboard = "Dashboard"
|
||||||
|
|
||||||
|
|
||||||
|
# 支持的存储类型
|
||||||
|
class StorageSchema(Enum):
|
||||||
|
# 存储类型
|
||||||
|
Local = "local"
|
||||||
|
Alipan = "alipan"
|
||||||
|
U115 = "u115"
|
||||||
|
Rclone = "rclone"
|
||||||
|
|||||||
Reference in New Issue
Block a user