add remote transfer

This commit is contained in:
jxxghp 2024-06-23 09:04:08 +08:00
parent e06e00204b
commit 1e1f80b6d9
8 changed files with 543 additions and 276 deletions

View File

@ -107,7 +107,7 @@ def delete_aliyun(fileitem: schemas.FileItem,
""" """
if not fileitem.fileid: if not fileitem.fileid:
return schemas.Response(success=False) return schemas.Response(success=False)
result = AliyunHelper().delete(fileitem.fileid) result = AliyunHelper().delete(drive_id=fileitem.drive_id, file_id=fileitem.fileid)
if result: if result:
return schemas.Response(success=True) return schemas.Response(success=True)
return schemas.Response(success=False) return schemas.Response(success=False)

View File

@ -48,6 +48,9 @@ def query_name(path: str, filetype: str,
@router.post("/manual", summary="手动转移", response_model=schemas.Response) @router.post("/manual", summary="手动转移", response_model=schemas.Response)
def manual_transfer(storage: str = "local", def manual_transfer(storage: str = "local",
path: str = None, path: str = None,
drive_id: str = None,
fileid: str = None,
filetype: str = None,
logid: int = None, logid: int = None,
target: str = None, target: str = None,
tmdbid: int = None, tmdbid: int = None,
@ -67,6 +70,9 @@ def manual_transfer(storage: str = "local",
手动转移文件或历史记录支持自定义剧集识别格式 手动转移文件或历史记录支持自定义剧集识别格式
:param storage: 存储类型local/aliyun/u115 :param storage: 存储类型local/aliyun/u115
:param path: 转移路径或文件 :param path: 转移路径或文件
:param drive_id: 云盘ID网盘等
:param fileid: 文件ID网盘等
:param filetype: 文件类型dir/file
:param logid: 转移历史记录ID :param logid: 转移历史记录ID
:param target: 目标路径 :param target: 目标路径
:param type_name: 媒体类型电影/电视剧 :param type_name: 媒体类型电影/电视剧
@ -123,6 +129,9 @@ def manual_transfer(storage: str = "local",
state, errormsg = transfer.manual_transfer( state, errormsg = transfer.manual_transfer(
storage=storage, storage=storage,
in_path=in_path, in_path=in_path,
drive_id=drive_id,
fileid=fileid,
filetype=filetype,
target=target, target=target,
tmdbid=tmdbid, tmdbid=tmdbid,
doubanid=doubanid, doubanid=doubanid,

View File

@ -399,6 +399,8 @@ class MediaChain(ChainBase, metaclass=Singleton):
if fileitem.type == "file" \ if fileitem.type == "file" \
and (not filepath.suffix or filepath.suffix.lower() not in settings.RMT_MEDIAEXT): and (not filepath.suffix or filepath.suffix.lower() not in settings.RMT_MEDIAEXT):
return return
if not mediainfo:
return
logger.info(f"开始刮削:{filepath} ...") logger.info(f"开始刮削:{filepath} ...")
if mediainfo.type == MediaType.MOVIE: if mediainfo.type == MediaType.MOVIE:
# 电影 # 电影

View File

@ -4,21 +4,24 @@ import threading
from pathlib import Path from pathlib import Path
from typing import List, Optional, Tuple, Union, Dict from typing import List, Optional, Tuple, Union, Dict
from app import schemas
from app.chain import ChainBase from app.chain import ChainBase
from app.chain.media import MediaChain from app.chain.media import MediaChain
from app.chain.tmdb import TmdbChain from app.chain.tmdb import TmdbChain
from app.core.config import settings from app.core.config import settings
from app.core.context import MediaInfo from app.core.context import MediaInfo
from app.core.meta import MetaBase from app.core.meta import MetaBase
from app.core.metainfo import MetaInfoPath from app.core.metainfo import MetaInfoPath, MetaInfo
from app.db.downloadhistory_oper import DownloadHistoryOper from app.db.downloadhistory_oper import DownloadHistoryOper
from app.db.models.downloadhistory import DownloadHistory 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.helper.aliyun 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.helper.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, \
@ -43,6 +46,7 @@ class TransferChain(ChainBase):
self.tmdbchain = TmdbChain() self.tmdbchain = TmdbChain()
self.systemconfig = SystemConfigOper() self.systemconfig = SystemConfigOper()
self.directoryhelper = DirectoryHelper() self.directoryhelper = DirectoryHelper()
self.all_exts = settings.RMT_MEDIAEXT + settings.RMT_SUBEXT + settings.RMT_AUDIO_TRACK_EXT
def recommend_name(self, meta: MetaBase, mediainfo: MediaInfo) -> Optional[str]: def recommend_name(self, meta: MetaBase, mediainfo: MediaInfo) -> Optional[str]:
""" """
@ -96,7 +100,7 @@ class TransferChain(ChainBase):
logger.info("下载器文件转移执行完成") logger.info("下载器文件转移执行完成")
return True return True
def do_transfer(self, storage: str, path: Path, def do_transfer(self, storage: str, path: Path, drive_id: str = None, fileid: str = None, filetype: str = None,
meta: MetaBase = None, mediainfo: MediaInfo = None, meta: MetaBase = None, mediainfo: MediaInfo = None,
download_hash: str = None, download_hash: str = None,
target: Path = None, transfer_type: str = None, target: Path = None, transfer_type: str = None,
@ -107,6 +111,9 @@ class TransferChain(ChainBase):
执行一个复杂目录的转移操作 执行一个复杂目录的转移操作
:param storage: 存储器 :param storage: 存储器
:param path: 待转移目录或文件 :param path: 待转移目录或文件
:param drive_id: 网盘ID
:param fileid: 文件ID
:param filetype: 文件类型
:param meta: 元数据 :param meta: 元数据
:param mediainfo: 媒体信息 :param mediainfo: 媒体信息
:param download_hash: 下载记录hash :param download_hash: 下载记录hash
@ -134,10 +141,43 @@ class TransferChain(ChainBase):
# 开始进度 # 开始进度
self.progress.start(ProgressKey.FileTransfer) self.progress.start(ProgressKey.FileTransfer)
# 本地存储
if storage == "local":
# 本地整理
result = self.__transfer_local(path=path, meta=meta, mediainfo=mediainfo,
formaterHandler=formaterHandler,
transfer_exclude_words=transfer_exclude_words,
min_filesize=min_filesize, transfer_type=transfer_type,
target=target, season=season, scrape=scrape,
download_hash=download_hash, force=force)
else:
# 网盘整理
result = self.__transfer_remote(storage=storage,
fileitem=schemas.FileItem(
path=str(path) + ("/" if filetype == "dir" else ""),
type=filetype,
drive_id=drive_id,
fileid=fileid,
name=path.name
),
meta=meta,
mediainfo=mediainfo)
# 结速进度
self.progress.end(ProgressKey.FileTransfer)
return result
def __transfer_local(self, path: Path, meta: MetaBase = None, mediainfo: MediaInfo = None,
formaterHandler: FormatParser = None, transfer_exclude_words: List[str] = None,
min_filesize: int = 0, transfer_type: str = None, target: Path = None,
season: int = None, scrape: bool = None, download_hash: str = None,
force: bool = False) -> Tuple[bool, str]:
"""
整理一个本地目录
"""
# 汇总错误信息 # 汇总错误信息
err_msgs: List[str] = [] err_msgs: List[str] = []
# 总文件数
total_num = 0
# 已处理数量 # 已处理数量
processed_num = 0 processed_num = 0
# 失败数量 # 失败数量
@ -145,8 +185,6 @@ class TransferChain(ChainBase):
# 跳过数量 # 跳过数量
skip_num = 0 skip_num = 0
# 本地存储
if storage == "local":
# 获取待转移路径清单 # 获取待转移路径清单
trans_paths = self.__get_trans_paths(path) trans_paths = self.__get_trans_paths(path)
if not trans_paths: if not trans_paths:
@ -419,10 +457,6 @@ class TransferChain(ChainBase):
'mediainfo': media, 'mediainfo': media,
'transferinfo': transfer_info 'transferinfo': transfer_info
}) })
else:
# TODO 网盘整理
pass
# 结束进度 # 结束进度
logger.info(f"{path} 转移完成,共 {total_num} 个文件," logger.info(f"{path} 转移完成,共 {total_num} 个文件,"
f"失败 {fail_num} 个,跳过 {skip_num}") f"失败 {fail_num} 个,跳过 {skip_num}")
@ -431,10 +465,215 @@ class TransferChain(ChainBase):
text=f"{path} 转移完成,共 {total_num} 个文件," text=f"{path} 转移完成,共 {total_num} 个文件,"
f"失败 {fail_num} 个,跳过 {skip_num}", f"失败 {fail_num} 个,跳过 {skip_num}",
key=ProgressKey.FileTransfer) key=ProgressKey.FileTransfer)
self.progress.end(ProgressKey.FileTransfer)
return True, "\n".join(err_msgs) return True, "\n".join(err_msgs)
def __transfer_remote(self, storage: str, fileitem: schemas.FileItem,
meta: MetaBase, mediainfo: MediaInfo) -> Tuple[bool, str]:
"""
整理一个远程目录
"""
def __list_files(_storage: str, _fileid: str,
_path: str = None, _drive_id: str = None) -> List[schemas.FileItem]:
"""
列出下级文件
"""
if _storage == "aliyun":
return AliyunHelper().list(drive_id=_drive_id, parent_file_id=_fileid, path=_path)
elif _storage == "u115":
return U115Helper().list(parent_file_id=_fileid, path=_path)
return []
def __rename_file(_storage: str, _fileid: str, _name: str) -> bool:
"""
重命名文件
"""
if _storage == "aliyun":
return AliyunHelper().rename(file_id=_fileid, name=_name)
elif _storage == "u115":
return U115Helper().rename(file_id=_fileid, name=_name)
return False
def __create_folder(_storage: str, _drive_id: str, _parent_fileid: str,
_name: str, _path: str) -> Optional[schemas.FileItem]:
"""
创建目录
"""
if _storage == "aliyun":
return AliyunHelper().create_folder(drive_id=_drive_id, parent_file_id=_parent_fileid,
name=_name, path=_path)
elif _storage == "u115":
return U115Helper().create_folder(parent_file_id=_parent_fileid, name=_name, path=_path)
return None
def __move_file(_storage: str, _drive_id: str, _fileid: str, _target_fileid: str) -> bool:
"""
移动文件
"""
if _storage == "aliyun":
return AliyunHelper().move(drive_id=_drive_id, file_id=_fileid, target_id=_target_fileid)
elif _storage == "u115":
return U115Helper().move(file_id=_fileid, target_id=_target_fileid)
return False
def __remove_dir(_storage: str, _drive_id: str, _fileid: str) -> bool:
"""
删除目录
"""
if _storage == "aliyun":
return AliyunHelper().delete(drive_id=_drive_id, file_id=_fileid)
elif _storage == "u115":
return U115Helper().delete(file_id=_fileid)
return False
logger.info(f"开始整理 {fileitem.path} ...")
self.progress.update(value=0,
text=f"正在整理 {fileitem.path} ...",
key=ProgressKey.FileTransfer)
# 重新识别
if not meta:
# 文件元数据
meta = MetaInfoPath(Path(fileitem.path))
if not mediainfo:
mediainfo = self.recognize_media(meta=meta)
if not mediainfo:
logger.warn(f"{fileitem.name} 未识别到媒体信息")
return False, f"{fileitem.name} 未识别到媒体信息"
# 获取完整的路径命名
full_names = self.recommend_name(meta=meta, mediainfo=mediainfo)
if not full_names:
logger.warn(f"{fileitem.path} 未获取到命名")
return False, f"{fileitem.path} 未获取到命名"
if mediainfo.type == MediaType.TV:
# 电视剧
[folder_name, season_name, file_name] = Path(full_names).parts
else:
# 电影
season_name = None
[folder_name, file_name] = Path(full_names).parts
# 如果是单个文件,则直接重命名
if fileitem.type == "file":
# 重命名文件
logger.info(f"正在整理 {fileitem.name} => {file_name} ...")
if not __rename_file(storage, fileitem.fileid, file_name):
logger.error(f"{fileitem.name} 重命名失败")
return False, f"{fileitem.name} 重命名失败"
logger.info(f"{fileitem.path} 整理完成")
else:
# 目录处理
if mediainfo.type == MediaType.MOVIE:
# 电影目录
# 重命名当前目录
logger.info(f"正在重命名 {fileitem.path} => {folder_name} ...")
if not __rename_file(_storage=storage, _fileid=fileitem.fileid, _name=folder_name):
logger.error(f"{fileitem.path} 重命名失败")
return False, f"{fileitem.path} 重命名失败"
logger.info(f"{fileitem.path} 重命名完成")
# 处理所有子文件或目录
files = __list_files(_storage=storage, _fileid=fileitem.fileid,
_drive_id=fileitem.drive_id, _path=fileitem.path)
if not files:
logger.info(f"{fileitem.path} 未找到文件,删除空目录")
if not __remove_dir(_storage=storage, _drive_id=fileitem.drive_id, _fileid=fileitem.fileid):
logger.error(f"{fileitem.path} 删除失败")
return False, f"{fileitem.path} 删除失败"
return True, ""
for file in files:
# 过滤不处理的文件
if file.type == "file" and str(file.extension) in ['nfo', 'jpg', 'png']:
continue
# 重新识别文件或目录
file_meta = MetaInfoPath(Path(file.path))
if not file_meta.name:
# 过滤掉无效文件
continue
file_media = self.recognize_media(meta=file_meta)
if not file_media:
logger.warn(f"{file.name} 未识别到媒体信息")
continue
# 整理这个文件或目录
self.__transfer_remote(storage=storage, fileitem=file, meta=file_meta, mediainfo=file_media)
else:
# 电视剧目录
# 判断当前目录类型
folder_meta = MetaInfo(fileitem.name)
if folder_meta.begin_season and not folder_meta.name:
# 季目录
logger.info(f"正在重命名 {fileitem.path} => {season_name} ...")
if not __rename_file(_storage=storage, _fileid=fileitem.fileid, _name=season_name):
logger.error(f"{fileitem.path} 重命名失败")
return False, f"{fileitem.path} 重命名失败"
logger.info(f"{fileitem.path} 重命名完成")
elif folder_meta.name:
# 根目录,重命名当前目录
logger.info(f"正在重命名 {fileitem.path} => {folder_name} ...")
if not __rename_file(_storage=storage, _fileid=fileitem.fileid, _name=folder_name):
logger.error(f"{fileitem.path} 重命名失败")
return False, f"{fileitem.path} 重命名失败"
logger.info(f"{fileitem.path} 重命名完成")
# 是否有季
if folder_meta.begin_season:
# 创建季目录
logger.info(f"正在创建目录 {fileitem.path}{season_name} ...")
season_dir = __create_folder(_storage=storage, _drive_id=fileitem.drive_id,
_parent_fileid=fileitem.fileid, _name=season_name,
_path=fileitem.path)
if not season_dir:
logger.error(f"{fileitem.path}/{season_name} 创建失败")
return False, f"{fileitem.path}/{season_name} 创建失败"
logger.info(f"{fileitem.path}/{season_name} 创建完成")
# 移动当前目录下的所有文件到季目录
files = __list_files(_storage=storage, _fileid=fileitem.fileid,
_drive_id=fileitem.drive_id, _path=fileitem.path)
if not files:
logger.error(f"{fileitem.path} 未找到文件,删除空目录")
if not __remove_dir(_storage=storage, _drive_id=fileitem.drive_id, _fileid=fileitem.fileid):
logger.error(f"{fileitem.path} 删除失败")
return False, f"{fileitem.path} 删除失败"
logger.info(f"{fileitem.path} 已删除")
return True, ""
for file in files:
if file.type == "dir":
continue
logger.info(f"正在移动 {file.path} => {season_dir.path}...")
if not __move_file(_storage=storage, _drive_id=fileitem.drive_id,
_fileid=file.fileid, _target_fileid=season_dir.fileid):
logger.error(f"{file.name} 移动失败")
return False, f"{file.name} 移动失败"
logger.info(f"{file.path} 移动完成")
# 修改当前目录为季目录
fileitem = season_dir
# 列出当前目录下所有的文件或目录,并进行重命名整理
files = __list_files(_storage=storage, _fileid=fileitem.fileid,
_drive_id=fileitem.drive_id, _path=fileitem.path)
if not files:
logger.info(f"{fileitem.path} 未找到文件,删除空目录")
if not __remove_dir(_storage=storage, _drive_id=fileitem.drive_id, _fileid=fileitem.fileid):
logger.error(f"{fileitem.path} 删除失败")
return False, f"{fileitem.path} 删除失败"
logger.info(f"{fileitem.path} 已删除")
return True, ""
for file in files:
# 过滤不处理的文件
if file.type == "file" and str(file.extension) in ['nfo', 'jpg', 'png']:
continue
# 重新识别文件或目录
file_meta = MetaInfoPath(Path(file.path))
file_media = self.recognize_media(meta=file_meta)
if not file_media:
logger.warn(f"{file.name} 未识别到媒体信息")
continue
# 整理这个文件或目录
self.__transfer_remote(storage=storage, fileitem=file, meta=file_meta, mediainfo=file_media)
logger.info(f"{fileitem.path} 整理完成")
self.progress.update(value=0,
text=f"{fileitem.path} 整理完成",
key=ProgressKey.FileTransfer)
return True, ""
@staticmethod @staticmethod
def __get_trans_paths(directory: Path): def __get_trans_paths(directory: Path):
""" """
@ -565,6 +804,9 @@ class TransferChain(ChainBase):
def manual_transfer(self, def manual_transfer(self,
storage: str, storage: str,
in_path: Path, in_path: Path,
drive_id: str = None,
fileid: str = None,
filetype: str = None,
target: Path = None, target: Path = None,
tmdbid: int = None, tmdbid: int = None,
doubanid: str = None, doubanid: str = None,
@ -579,6 +821,9 @@ class TransferChain(ChainBase):
手动转移支持复杂条件带进度显示 手动转移支持复杂条件带进度显示
:param storage: 存储器 :param storage: 存储器
:param in_path: 源文件路径 :param in_path: 源文件路径
:param drive_id: 网盘ID
:param fileid: 文件ID
:param filetype: 文件类型
:param target: 目标路径 :param target: 目标路径
:param tmdbid: TMDB ID :param tmdbid: TMDB ID
:param doubanid: 豆瓣ID :param doubanid: 豆瓣ID
@ -607,6 +852,9 @@ class TransferChain(ChainBase):
state, errmsg = self.do_transfer( state, errmsg = self.do_transfer(
storage=storage, storage=storage,
path=in_path, path=in_path,
drive_id=drive_id,
fileid=fileid,
filetype=filetype,
mediainfo=mediainfo, mediainfo=mediainfo,
target=target, target=target,
transfer_type=transfer_type, transfer_type=transfer_type,
@ -626,6 +874,9 @@ class TransferChain(ChainBase):
# 没有输入TMDBID时按文件识别 # 没有输入TMDBID时按文件识别
state, errmsg = self.do_transfer(storage=storage, state, errmsg = self.do_transfer(storage=storage,
path=in_path, path=in_path,
drive_id=drive_id,
fileid=fileid,
filetype=filetype,
target=target, target=target,
transfer_type=transfer_type, transfer_type=transfer_type,
season=season, season=season,

View File

@ -438,7 +438,7 @@ class AliyunHelper:
self.__handle_error(res, "创建目录") self.__handle_error(res, "创建目录")
return None return None
def delete(self, file_id: str) -> bool: def delete(self, drive_id: str, file_id: str) -> bool:
""" """
删除文件 删除文件
""" """
@ -447,7 +447,7 @@ class AliyunHelper:
return False return False
headers = self.__get_headers(params) headers = self.__get_headers(params)
res = RequestUtils(headers=headers, timeout=10).post_res(self.delete_file_url, json={ res = RequestUtils(headers=headers, timeout=10).post_res(self.delete_file_url, json={
"drive_id": params.get("resourceDriveId"), "drive_id": drive_id,
"file_id": file_id "file_id": file_id
}) })
if res: if res:
@ -571,7 +571,7 @@ class AliyunHelper:
return None return None
# 获取上传参数 # 获取上传参数
result = res.json() result = res.json()
if result.get("'exist'"): if result.get("exist"):
logger.info(f"文件{result.get('file_name')}已存在,无需上传") logger.info(f"文件{result.get('file_name')}已存在,无需上传")
return schemas.FileItem( return schemas.FileItem(
drive_id=result.get("drive_id"), drive_id=result.get("drive_id"),

View File

@ -775,14 +775,15 @@ class DoubanModule(_ModuleBase):
return None return None
return self.scraper.get_metadata_nfo(mediainfo=mediainfo, season=season) return self.scraper.get_metadata_nfo(mediainfo=mediainfo, season=season)
def metadata_img(self, mediainfo: MediaInfo, **kwargs) -> Optional[dict]: def metadata_img(self, mediainfo: MediaInfo, season: int = None) -> Optional[dict]:
""" """
获取图片名称和url 获取图片名称和url
:param mediainfo: 媒体信息 :param mediainfo: 媒体信息
:param season: 季号
""" """
if settings.SCRAP_SOURCE != "douban": if settings.SCRAP_SOURCE != "douban":
return None return None
return self.scraper.get_metadata_img(mediainfo=mediainfo) return self.scraper.get_metadata_img(mediainfo=mediainfo, season=season)
def obtain_images(self, mediainfo: MediaInfo) -> Optional[MediaInfo]: def obtain_images(self, mediainfo: MediaInfo) -> Optional[MediaInfo]:
""" """

View File

@ -39,12 +39,16 @@ class DoubanScraper:
return None return None
@staticmethod @staticmethod
def get_metadata_img(mediainfo: MediaInfo) -> Optional[dict]: def get_metadata_img(mediainfo: MediaInfo, season: int = None) -> Optional[dict]:
""" """
获取图片内容 获取图片内容
:param mediainfo: 媒体信息 :param mediainfo: 媒体信息
:param season: 季号
""" """
ret_dict = {} ret_dict = {}
if season:
# 豆瓣无季图片
return {}
if mediainfo.poster_path: if mediainfo.poster_path:
ret_dict[f"poster{Path(mediainfo.poster_path).suffix}"] = mediainfo.poster_path ret_dict[f"poster{Path(mediainfo.poster_path).suffix}"] = mediainfo.poster_path
if mediainfo.backdrop_path: if mediainfo.backdrop_path:

View File

@ -65,7 +65,16 @@ class TmdbScraper:
:param season: 季号 :param season: 季号
""" """
images = {} images = {}
if mediainfo.type == MediaType.MOVIE: if season:
# 只需要季的图片
seasoninfo = self.tmdb.get_tv_season_detail(mediainfo.tmdb_id, season)
if seasoninfo:
# TMDB季poster图片
poster_name, poster_url = self.get_season_poster(seasoninfo, season)
if poster_name and poster_url:
images[poster_name] = poster_url
return images
# 主媒体图片
for attr_name, attr_value in vars(mediainfo).items(): for attr_name, attr_value in vars(mediainfo).items():
if attr_value \ if attr_value \
and attr_name.endswith("_path") \ and attr_name.endswith("_path") \
@ -74,15 +83,6 @@ class TmdbScraper:
and attr_value.startswith("http"): and attr_value.startswith("http"):
image_name = attr_name.replace("_path", "") + Path(attr_value).suffix image_name = attr_name.replace("_path", "") + Path(attr_value).suffix
images[image_name] = attr_value images[image_name] = attr_value
else:
if season:
# 查询季信息
seasoninfo = self.tmdb.get_tv_season_detail(mediainfo.tmdb_id, season)
if seasoninfo:
# TMDB季poster图片
poster_name, poster_url = self.get_season_poster(seasoninfo, season)
if poster_name and poster_url:
images[poster_name] = poster_url
return images return images
@staticmethod @staticmethod