diff --git a/app/core/config.py b/app/core/config.py index 38e6e4f6..ab426419 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -128,9 +128,9 @@ class Settings(BaseSettings): # 豆瓣用户ID,用于同步豆瓣数据,使用,分隔 DOUBAN_USER_IDS: str = "" # 电影重命名格式 - MOVIE_RENAME_FORMAT: str = "{title} ({year})/{title} ({year})-{part} - {videoFormat}" + MOVIE_RENAME_FORMAT: str = "{{title}} ({{year}})/{{title}} ({{year}})-{{part}} - {{videoFormat}}{{fileExt}}" # 电视剧重命名格式 - TV_RENAME_FORMAT: str = "{title} ({year})/Season {season}/{title} - {season_episode}-{part} - 第 {episode} 集" + TV_RENAME_FORMAT: str = "{{title}} ({{year}})/Season {{season}}/{{title}} - {{season_episode}}-{{part}} - 第 {{episode}} 集{{fileExt}}" @property def INNER_CONFIG_PATH(self): diff --git a/app/modules/filetransfer/__init__.py b/app/modules/filetransfer/__init__.py index 97b87e22..c1e5e9b7 100644 --- a/app/modules/filetransfer/__init__.py +++ b/app/modules/filetransfer/__init__.py @@ -3,11 +3,14 @@ from pathlib import Path from threading import Lock from typing import Optional, List, Tuple, Union +from jinja2 import Template + from app.core import MediaInfo, MetaInfo, settings from app.core.meta import MetaBase from app.log import logger from app.modules import _ModuleBase from app.utils.system import SystemUtils +from app.utils.types import MediaType lock = Lock() @@ -22,12 +25,22 @@ class FileTransferModule(_ModuleBase): def transfer(self, path: str, mediainfo: MediaInfo) -> Optional[bool]: """ - TODO 文件转移 + 文件转移 :param path: 文件路径 :param mediainfo: 识别的媒体信息 :return: 成功或失败 """ - pass + if not settings.LIBRARY_PATH: + logger.error("未设置媒体库目录,无法转移文件") + return None + state, msg = self.transfer_media(in_path=Path(path), + meidainfo=mediainfo, + rmt_mode=settings.TRANSFER_TYPE, + target_dir=Path(settings.LIBRARY_PATH)) + if not state: + logger.error(msg) + + return state @staticmethod def __transfer_command(file_item: Path, target_file: Path, rmt_mode) -> int: @@ -280,6 +293,19 @@ class FileTransferModule(_ModuleBase): rmt_mode=rmt_mode, over_flag=over_flag) + @staticmethod + def __is_bluray_dir(dir_path: Path) -> bool: + """ + 判断是否为蓝光原盘目录 + """ + # 蓝光原盘目录必备的文件或文件夹 + required_files = ['BDMV', 'CERTIFICATE'] + # 检查目录下是否存在所需文件或文件夹 + for item in required_files: + if (dir_path / item).exists(): + return True + return False + def transfer_media(self, in_path: Path, meidainfo: MediaInfo, @@ -294,15 +320,119 @@ class FileTransferModule(_ModuleBase): :param meidainfo: 媒体信息 :return: 处理状态,错误信息 """ - pass + # 检查目录路径 + if not in_path.exists(): + return False, f"路径不存在:{in_path}" - def __get_naming_dict(self, meta: MetaBase, mediainfo: MediaInfo) -> dict: + if not target_dir.exists(): + return False, f"目标路径不存在:{target_dir}" + + # 目的目录加上类型和二级分类 + target_dir = target_dir / meidainfo.type.value / meidainfo.category + + # 重命名格式 + rename_format = settings.MOVIE_RENAME_FORMAT \ + if meidainfo.type == MediaType.MOVIE else settings.TV_RENAME_FORMAT + + # 判断是否为蓝光原盘 + bluray_flag = self.__is_bluray_dir(in_path) + if bluray_flag: + # 识别目录名称,不包括后缀 + meta = MetaInfo(in_path.stem) + # 目的路径 + new_path = self.get_rename_path( + path=target_dir, + template_string=rename_format, + rename_dict=self.__get_naming_dict(meta=meta, + mediainfo=meidainfo) + ).parent + # 转移蓝光原盘 + retcode = self.__transfer_bluray_dir(file_path=in_path, + new_path=new_path, + rmt_mode=rmt_mode) + if retcode != 0: + return False, f"蓝光原盘转移失败,错误码:{retcode}" + else: + return True, "" + else: + # 获取文件清单 + transfer_files: List[Path] = SystemUtils.list_files_with_extensions(in_path, settings.RMT_MEDIAEXT) + if len(transfer_files) == 0: + return False, f"目录下没有找到可转移的文件:{in_path}" + # 转移所有文件 + for transfer_file in transfer_files: + # 识别文件元数据,不包含后缀 + meta = MetaInfo(transfer_file.stem) + # 目的文件名 + new_file = self.get_rename_path( + path=target_dir, + template_string=rename_format, + rename_dict=self.__get_naming_dict(meta=meta, + mediainfo=meidainfo, + file_ext=transfer_file.suffix) + ) + # 判断是否要覆盖 + overflag = False + if new_file.exists(): + if new_file.stat().st_size < transfer_file.stat().st_size: + logger.info(f"目标文件已存在,但文件大小更小,将覆盖:{new_file}") + overflag = True + # 转移文件 + retcode = self.__transfer_file(file_item=transfer_file, + new_file=new_file, + rmt_mode=rmt_mode, + over_flag=overflag) + if retcode != 0: + return False, f"文件转移失败,错误码:{retcode}" + + return True, "" + + @staticmethod + def __get_naming_dict(meta: MetaBase, mediainfo: MediaInfo, file_ext: str = None) -> dict: """ 根据媒体信息,返回Format字典 :param meta: 文件元数据 :param mediainfo: 识别的媒体信息 + :param file_ext: 文件扩展名 """ - pass + return { + # 标题 + "title": mediainfo.title, + # 原文件名 + "original_name": meta.org_string, + # 原语种标题 + "original_title": mediainfo.original_title, + # 识别名称 + "name": meta.get_name(), + # 年份 + "year": mediainfo.year, + # 版本 + "edition": meta.get_edtion_string(), + # 分辨率 + "videoFormat": meta.resource_pix, + # 制作组/字幕组 + "releaseGroup": meta.resource_team, + # 特效 + "effect": meta.resource_effect, + # 视频编码 + "videoCodec": meta.video_encode, + # 音频编码 + "audioCodec": meta.audio_encode, + # TMDBID + "tmdbid": mediainfo.tmdb_id, + # IMDBID + "imdbid": mediainfo.imdb_id, + # 季号 + "season": meta.get_season_seq(), + # 集号 + "episode": meta.get_episode_seqs(), + # 季集 SxxExx + "season_episode": "%s%s" % (meta.get_season_item(), meta.get_episode_items()), + # 段/节 + "part": meta.part, + # 文件后缀 + "fileExt": file_ext + } def get_movie_dest_path(self, meta: MetaBase, mediainfo: MediaInfo) -> Tuple[str, str]: """ @@ -317,3 +447,15 @@ class FileTransferModule(_ModuleBase): :return: 电视剧目录、季目录、集名称 """ pass + + @staticmethod + def get_rename_path(path: Path, template_string: str, rename_dict: dict) -> Path: + """ + 生成重命名后的完整路径 + """ + # 创建jinja2模板对象 + template = Template(template_string) + # 渲染生成的字符串 + render_str = template.render(rename_dict) + # 目的路径 + return path / render_str