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,236 +185,194 @@ class TransferChain(ChainBase):
# 跳过数量 # 跳过数量
skip_num = 0 skip_num = 0
# 本地存储 # 获取待转移路径清单
if storage == "local": trans_paths = self.__get_trans_paths(path)
# 获取待转移路径清单 if not trans_paths:
trans_paths = self.__get_trans_paths(path) logger.warn(f"{path.name} 没有找到可转移的媒体文件")
if not trans_paths: return False, f"{path.name} 没有找到可转移的媒体文件"
logger.warn(f"{path.name} 没有找到可转移的媒体文件") # 目录所有文件清单
return False, f"{path.name} 没有找到可转移的媒体文件" transfer_files = SystemUtils.list_files(directory=path,
# 目录所有文件清单 extensions=settings.RMT_MEDIAEXT,
transfer_files = SystemUtils.list_files(directory=path, min_filesize=min_filesize)
if formaterHandler:
# 有集自定义格式,过滤文件
transfer_files = [f for f in transfer_files if formaterHandler.match(f.name)]
# 总文件数
total_num = len(transfer_files)
self.progress.update(value=0,
text=f"开始转移 {path},共 {total_num} 个文件 ...",
key=ProgressKey.FileTransfer)
# 处理所有待转移目录或文件,默认一个转移路径或文件只有一个媒体信息
for trans_path in trans_paths:
# 汇总季集清单
season_episodes: Dict[Tuple, List[int]] = {}
# 汇总元数据
metas: Dict[Tuple, MetaBase] = {}
# 汇总媒体信息
medias: Dict[Tuple, MediaInfo] = {}
# 汇总转移信息
transfers: Dict[Tuple, TransferInfo] = {}
# 如果是目录且不是⼀蓝光原盘,获取所有文件并转移
if (not trans_path.is_file()
and not SystemUtils.is_bluray_dir(trans_path)):
# 遍历获取下载目录所有文件
file_paths = SystemUtils.list_files(directory=trans_path,
extensions=settings.RMT_MEDIAEXT, extensions=settings.RMT_MEDIAEXT,
min_filesize=min_filesize) min_filesize=min_filesize)
else:
file_paths = [trans_path]
if formaterHandler: if formaterHandler:
# 有集自定义格式,过滤文件 # 有集自定义格式,过滤文件
transfer_files = [f for f in transfer_files if formaterHandler.match(f.name)] file_paths = [f for f in file_paths if formaterHandler.match(f.name)]
# 总文件数 # 转移所有文件
total_num = len(transfer_files) for file_path in file_paths:
self.progress.update(value=0, # 回收站及隐藏的文件不处理
text=f"开始转移 {path},共 {total_num} 个文件 ...", file_path_str = str(file_path)
key=ProgressKey.FileTransfer) if file_path_str.find('/@Recycle/') != -1 \
or file_path_str.find('/#recycle/') != -1 \
or file_path_str.find('/.') != -1 \
or file_path_str.find('/@eaDir') != -1:
logger.debug(f"{file_path_str} 是回收站或隐藏的文件")
# 计数
processed_num += 1
skip_num += 1
continue
# 处理所有待转移目录或文件,默认一个转移路径或文件只有一个媒体信息 # 整理屏蔽词不处理
for trans_path in trans_paths: is_blocked = False
# 汇总季集清单 if transfer_exclude_words:
season_episodes: Dict[Tuple, List[int]] = {} for keyword in transfer_exclude_words:
# 汇总元数据 if not keyword:
metas: Dict[Tuple, MetaBase] = {}
# 汇总媒体信息
medias: Dict[Tuple, MediaInfo] = {}
# 汇总转移信息
transfers: Dict[Tuple, TransferInfo] = {}
# 如果是目录且不是⼀蓝光原盘,获取所有文件并转移
if (not trans_path.is_file()
and not SystemUtils.is_bluray_dir(trans_path)):
# 遍历获取下载目录所有文件
file_paths = SystemUtils.list_files(directory=trans_path,
extensions=settings.RMT_MEDIAEXT,
min_filesize=min_filesize)
else:
file_paths = [trans_path]
if formaterHandler:
# 有集自定义格式,过滤文件
file_paths = [f for f in file_paths if formaterHandler.match(f.name)]
# 转移所有文件
for file_path in file_paths:
# 回收站及隐藏的文件不处理
file_path_str = str(file_path)
if file_path_str.find('/@Recycle/') != -1 \
or file_path_str.find('/#recycle/') != -1 \
or file_path_str.find('/.') != -1 \
or file_path_str.find('/@eaDir') != -1:
logger.debug(f"{file_path_str} 是回收站或隐藏的文件")
# 计数
processed_num += 1
skip_num += 1
continue
# 整理屏蔽词不处理
is_blocked = False
if transfer_exclude_words:
for keyword in transfer_exclude_words:
if not keyword:
continue
if keyword and re.search(r"%s" % keyword, file_path_str, re.IGNORECASE):
logger.info(f"{file_path} 命中整理屏蔽词 {keyword},不处理")
is_blocked = True
break
if is_blocked:
err_msgs.append(f"{file_path.name} 命中整理屏蔽词")
# 计数
processed_num += 1
skip_num += 1
continue
# 转移成功的不再处理
if not force:
transferd = self.transferhis.get_by_src(file_path_str)
if transferd and transferd.status:
logger.info(f"{file_path} 已成功转移过,如需重新处理,请删除历史记录。")
# 计数
processed_num += 1
skip_num += 1
continue continue
if keyword and re.search(r"%s" % keyword, file_path_str, re.IGNORECASE):
logger.info(f"{file_path} 命中整理屏蔽词 {keyword},不处理")
is_blocked = True
break
if is_blocked:
err_msgs.append(f"{file_path.name} 命中整理屏蔽词")
# 计数
processed_num += 1
skip_num += 1
continue
# 更新进度 # 转移成功的不再处理
self.progress.update(value=processed_num / total_num * 100, if not force:
text=f"正在转移 {processed_num + 1}/{total_num}{file_path.name} ...", transferd = self.transferhis.get_by_src(file_path_str)
key=ProgressKey.FileTransfer) if transferd and transferd.status:
logger.info(f"{file_path} 已成功转移过,如需重新处理,请删除历史记录。")
if not meta:
# 文件元数据
file_meta = MetaInfoPath(file_path)
else:
file_meta = meta
# 合并季
if season is not None:
file_meta.begin_season = season
if not file_meta:
logger.error(f"{file_path} 无法识别有效信息")
err_msgs.append(f"{file_path} 无法识别有效信息")
# 计数 # 计数
processed_num += 1 processed_num += 1
fail_num += 1 skip_num += 1
continue continue
# 自定义识别 # 更新进度
if formaterHandler: self.progress.update(value=processed_num / total_num * 100,
# 开始集、结束集、PART text=f"正在转移 {processed_num + 1}/{total_num}{file_path.name} ...",
begin_ep, end_ep, part = formaterHandler.split_episode(file_path.name) key=ProgressKey.FileTransfer)
if begin_ep is not None:
file_meta.begin_episode = begin_ep
file_meta.part = part
if end_ep is not None:
file_meta.end_episode = end_ep
if not mediainfo: if not meta:
# 识别媒体信息 # 文件元数据
file_mediainfo = self.mediachain.recognize_by_meta(file_meta) file_meta = MetaInfoPath(file_path)
else: else:
file_mediainfo = mediainfo file_meta = meta
if not file_mediainfo: # 合并季
logger.warn(f'{file_path} 未识别到媒体信息') if season is not None:
# 新增转移失败历史记录 file_meta.begin_season = season
his = self.transferhis.add_fail(
src_path=file_path,
mode=transfer_type,
meta=file_meta,
download_hash=download_hash
)
self.post_message(Notification(
mtype=NotificationType.Manual,
title=f"{file_path.name} 未识别到媒体信息,无法入库!",
text=f"回复:```\n/redo {his.id} [tmdbid]|[类型]\n``` 手动识别转移。",
link=settings.MP_DOMAIN('#/history')
))
# 计数
processed_num += 1
fail_num += 1
continue
# 如果未开启新增已入库媒体是否跟随TMDB信息变化则根据tmdbid查询之前的title if not file_meta:
if not settings.SCRAP_FOLLOW_TMDB: logger.error(f"{file_path} 无法识别有效信息")
transfer_history = self.transferhis.get_by_type_tmdbid(tmdbid=file_mediainfo.tmdb_id, err_msgs.append(f"{file_path} 无法识别有效信息")
mtype=file_mediainfo.type.value) # 计数
if transfer_history: processed_num += 1
file_mediainfo.title = transfer_history.title fail_num += 1
continue
logger.info(f"{file_path.name} 识别为:{file_mediainfo.type.value} {file_mediainfo.title_year}") # 自定义识别
if formaterHandler:
# 开始集、结束集、PART
begin_ep, end_ep, part = formaterHandler.split_episode(file_path.name)
if begin_ep is not None:
file_meta.begin_episode = begin_ep
file_meta.part = part
if end_ep is not None:
file_meta.end_episode = end_ep
# 获取集数据 if not mediainfo:
if file_mediainfo.type == MediaType.TV: # 识别媒体信息
if file_meta.begin_season is None: file_mediainfo = self.mediachain.recognize_by_meta(file_meta)
file_meta.begin_season = 1 else:
file_mediainfo.season = file_mediainfo.season or file_meta.begin_season file_mediainfo = mediainfo
episodes_info = self.tmdbchain.tmdb_episodes(
tmdbid=file_mediainfo.tmdb_id,
season=file_mediainfo.season
)
else:
episodes_info = None
# 获取下载hash if not file_mediainfo:
if not download_hash: logger.warn(f'{file_path} 未识别到媒体信息')
download_file = self.downloadhis.get_file_by_fullpath(file_path_str) # 新增转移失败历史记录
if download_file: his = self.transferhis.add_fail(
download_hash = download_file.download_hash src_path=file_path,
mode=transfer_type,
meta=file_meta,
download_hash=download_hash
)
self.post_message(Notification(
mtype=NotificationType.Manual,
title=f"{file_path.name} 未识别到媒体信息,无法入库!",
text=f"回复:```\n/redo {his.id} [tmdbid]|[类型]\n``` 手动识别转移。",
link=settings.MP_DOMAIN('#/history')
))
# 计数
processed_num += 1
fail_num += 1
continue
# 执行转移 # 如果未开启新增已入库媒体是否跟随TMDB信息变化则根据tmdbid查询之前的title
transferinfo: TransferInfo = self.transfer(meta=file_meta, if not settings.SCRAP_FOLLOW_TMDB:
mediainfo=file_mediainfo, transfer_history = self.transferhis.get_by_type_tmdbid(tmdbid=file_mediainfo.tmdb_id,
path=file_path, mtype=file_mediainfo.type.value)
transfer_type=transfer_type, if transfer_history:
target=target, file_mediainfo.title = transfer_history.title
episodes_info=episodes_info,
scrape=scrape)
if not transferinfo:
logger.error("文件转移模块运行失败")
return False, "文件转移模块运行失败"
if not transferinfo.success:
# 转移失败
logger.warn(f"{file_path.name} 入库失败:{transferinfo.message}")
err_msgs.append(f"{file_path.name} {transferinfo.message}")
# 新增转移失败历史记录
self.transferhis.add_fail(
src_path=file_path,
mode=transfer_type,
download_hash=download_hash,
meta=file_meta,
mediainfo=file_mediainfo,
transferinfo=transferinfo
)
# 发送消息
self.post_message(Notification(
mtype=NotificationType.Manual,
title=f"{file_mediainfo.title_year} {file_meta.season_episode} 入库失败!",
text=f"原因:{transferinfo.message or '未知'}",
image=file_mediainfo.get_message_image(),
link=settings.MP_DOMAIN('#/history')
))
# 计数
processed_num += 1
fail_num += 1
continue
# 汇总信息 logger.info(f"{file_path.name} 识别为:{file_mediainfo.type.value} {file_mediainfo.title_year}")
mkey = (file_mediainfo.tmdb_id, file_meta.begin_season)
if mkey not in medias:
# 新增信息
metas[mkey] = file_meta
medias[mkey] = file_mediainfo
season_episodes[mkey] = file_meta.episode_list
transfers[mkey] = transferinfo
else:
# 合并季集清单
season_episodes[mkey] = list(set(season_episodes[mkey] + file_meta.episode_list))
# 合并转移数据
transfers[mkey].file_count += transferinfo.file_count
transfers[mkey].total_size += transferinfo.total_size
transfers[mkey].file_list.extend(transferinfo.file_list)
transfers[mkey].file_list_new.extend(transferinfo.file_list_new)
transfers[mkey].fail_list.extend(transferinfo.fail_list)
# 新增转移成功历史记录 # 获取集数据
self.transferhis.add_success( if file_mediainfo.type == MediaType.TV:
if file_meta.begin_season is None:
file_meta.begin_season = 1
file_mediainfo.season = file_mediainfo.season or file_meta.begin_season
episodes_info = self.tmdbchain.tmdb_episodes(
tmdbid=file_mediainfo.tmdb_id,
season=file_mediainfo.season
)
else:
episodes_info = None
# 获取下载hash
if not download_hash:
download_file = self.downloadhis.get_file_by_fullpath(file_path_str)
if download_file:
download_hash = download_file.download_hash
# 执行转移
transferinfo: TransferInfo = self.transfer(meta=file_meta,
mediainfo=file_mediainfo,
path=file_path,
transfer_type=transfer_type,
target=target,
episodes_info=episodes_info,
scrape=scrape)
if not transferinfo:
logger.error("文件转移模块运行失败")
return False, "文件转移模块运行失败"
if not transferinfo.success:
# 转移失败
logger.warn(f"{file_path.name} 入库失败:{transferinfo.message}")
err_msgs.append(f"{file_path.name} {transferinfo.message}")
# 新增转移失败历史记录
self.transferhis.add_fail(
src_path=file_path, src_path=file_path,
mode=transfer_type, mode=transfer_type,
download_hash=download_hash, download_hash=download_hash,
@ -382,47 +380,83 @@ class TransferChain(ChainBase):
mediainfo=file_mediainfo, mediainfo=file_mediainfo,
transferinfo=transferinfo transferinfo=transferinfo
) )
# 刮削单个文件 # 发送消息
if transferinfo.need_scrape: self.post_message(Notification(
self.scrape_metadata(path=transferinfo.target_path, mtype=NotificationType.Manual,
mediainfo=file_mediainfo, title=f"{file_mediainfo.title_year} {file_meta.season_episode} 入库失败!",
transfer_type=transfer_type, text=f"原因:{transferinfo.message or '未知'}",
metainfo=file_meta) image=file_mediainfo.get_message_image(),
# 更新进度 link=settings.MP_DOMAIN('#/history')
))
# 计数
processed_num += 1 processed_num += 1
self.progress.update(value=processed_num / total_num * 100, fail_num += 1
text=f"{file_path.name} 转移完成", continue
key=ProgressKey.FileTransfer)
# 目录或文件转移完成 # 汇总信息
self.progress.update(text=f"{trans_path} 转移完成,正在执行后续处理 ...", mkey = (file_mediainfo.tmdb_id, file_meta.begin_season)
if mkey not in medias:
# 新增信息
metas[mkey] = file_meta
medias[mkey] = file_mediainfo
season_episodes[mkey] = file_meta.episode_list
transfers[mkey] = transferinfo
else:
# 合并季集清单
season_episodes[mkey] = list(set(season_episodes[mkey] + file_meta.episode_list))
# 合并转移数据
transfers[mkey].file_count += transferinfo.file_count
transfers[mkey].total_size += transferinfo.total_size
transfers[mkey].file_list.extend(transferinfo.file_list)
transfers[mkey].file_list_new.extend(transferinfo.file_list_new)
transfers[mkey].fail_list.extend(transferinfo.fail_list)
# 新增转移成功历史记录
self.transferhis.add_success(
src_path=file_path,
mode=transfer_type,
download_hash=download_hash,
meta=file_meta,
mediainfo=file_mediainfo,
transferinfo=transferinfo
)
# 刮削单个文件
if transferinfo.need_scrape:
self.scrape_metadata(path=transferinfo.target_path,
mediainfo=file_mediainfo,
transfer_type=transfer_type,
metainfo=file_meta)
# 更新进度
processed_num += 1
self.progress.update(value=processed_num / total_num * 100,
text=f"{file_path.name} 转移完成",
key=ProgressKey.FileTransfer) key=ProgressKey.FileTransfer)
# 执行后续处理 # 目录或文件转移完成
for mkey, media in medias.items(): self.progress.update(text=f"{trans_path} 转移完成,正在执行后续处理 ...",
transfer_meta = metas[mkey] key=ProgressKey.FileTransfer)
transfer_info = transfers[mkey]
# 媒体目录
if transfer_info.target_path.is_file():
transfer_info.target_path = transfer_info.target_path.parent
# 发送通知
se_str = None
if media.type == MediaType.TV:
se_str = f"{transfer_meta.season} {StringUtils.format_ep(season_episodes[mkey])}"
self.send_transfer_message(meta=transfer_meta,
mediainfo=media,
transferinfo=transfer_info,
season_episode=se_str)
# 广播事件
self.eventmanager.send_event(EventType.TransferComplete, {
'meta': transfer_meta,
'mediainfo': media,
'transferinfo': transfer_info
})
else:
# TODO 网盘整理
pass
# 执行后续处理
for mkey, media in medias.items():
transfer_meta = metas[mkey]
transfer_info = transfers[mkey]
# 媒体目录
if transfer_info.target_path.is_file():
transfer_info.target_path = transfer_info.target_path.parent
# 发送通知
se_str = None
if media.type == MediaType.TV:
se_str = f"{transfer_meta.season} {StringUtils.format_ep(season_episodes[mkey])}"
self.send_transfer_message(meta=transfer_meta,
mediainfo=media,
transferinfo=transfer_info,
season_episode=se_str)
# 广播事件
self.eventmanager.send_event(EventType.TransferComplete, {
'meta': transfer_meta,
'mediainfo': media,
'transferinfo': transfer_info
})
# 结束进度 # 结束进度
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,24 +65,24 @@ class TmdbScraper:
:param season: 季号 :param season: 季号
""" """
images = {} images = {}
if mediainfo.type == MediaType.MOVIE: if season:
for attr_name, attr_value in vars(mediainfo).items(): # 只需要季的图片
if attr_value \ seasoninfo = self.tmdb.get_tv_season_detail(mediainfo.tmdb_id, season)
and attr_name.endswith("_path") \ if seasoninfo:
and attr_value \ # TMDB季poster图片
and isinstance(attr_value, str) \ poster_name, poster_url = self.get_season_poster(seasoninfo, season)
and attr_value.startswith("http"): if poster_name and poster_url:
image_name = attr_name.replace("_path", "") + Path(attr_value).suffix images[poster_name] = poster_url
images[image_name] = attr_value return images
else: # 主媒体图片
if season: for attr_name, attr_value in vars(mediainfo).items():
# 查询季信息 if attr_value \
seasoninfo = self.tmdb.get_tv_season_detail(mediainfo.tmdb_id, season) and attr_name.endswith("_path") \
if seasoninfo: and attr_value \
# TMDB季poster图片 and isinstance(attr_value, str) \
poster_name, poster_url = self.get_season_poster(seasoninfo, season) and attr_value.startswith("http"):
if poster_name and poster_url: image_name = attr_name.replace("_path", "") + Path(attr_value).suffix
images[poster_name] = poster_url images[image_name] = attr_value
return images return images
@staticmethod @staticmethod