From d9c6375252843aa1e8460499794ef413219a27ee Mon Sep 17 00:00:00 2001 From: jxxghp Date: Wed, 22 May 2024 20:02:14 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E7=9B=AE=E5=BD=95=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E9=87=8D=E5=A4=A7=E8=B0=83=E6=95=B4=EF=BC=8C=E8=B0=A8?= =?UTF-8?q?=E6=85=8E=E6=9B=B4=E6=96=B0=E5=88=B0dev?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/dashboard.py | 12 +- app/api/servarr.py | 3 +- app/chain/download.py | 40 ++--- app/chain/media.py | 4 +- app/chain/subscribe.py | 4 +- app/chain/transfer.py | 2 +- app/core/config.py | 89 +++------- app/core/meta/metavideo.py | 3 +- app/core/metainfo.py | 1 - app/db/models/__init__.py | 2 +- app/helper/directory.py | 93 +++++++++++ app/modules/filetransfer/__init__.py | 197 +++++++---------------- app/modules/qbittorrent/__init__.py | 4 +- app/scheduler.py | 6 +- app/schemas/filetransfer.py | 2 +- app/schemas/transfer.py | 2 + database/versions/a40261701909_1_0_20.py | 140 ++++++++++++++++ 17 files changed, 359 insertions(+), 245 deletions(-) create mode 100644 app/helper/directory.py create mode 100644 database/versions/a40261701909_1_0_20.py diff --git a/app/api/endpoints/dashboard.py b/app/api/endpoints/dashboard.py index eea76337..a452c75b 100644 --- a/app/api/endpoints/dashboard.py +++ b/app/api/endpoints/dashboard.py @@ -1,3 +1,4 @@ +from pathlib import Path from typing import Any, List, Optional from fastapi import APIRouter, Depends @@ -5,10 +6,10 @@ from sqlalchemy.orm import Session from app import schemas from app.chain.dashboard import DashboardChain -from app.core.config import settings from app.core.security import verify_token, verify_uri_token from app.db import get_db from app.db.models.transferhistory import TransferHistory +from app.helper.directory import DirectoryHelper from app.scheduler import Scheduler from app.utils.system import SystemUtils @@ -47,7 +48,8 @@ def storage(_: schemas.TokenPayload = Depends(verify_token)) -> Any: """ 查询存储空间信息 """ - total_storage, free_storage = SystemUtils.space_usage(settings.LIBRARY_PATHS) + library_dirs = DirectoryHelper().get_library_dirs() + total_storage, free_storage = SystemUtils.space_usage([Path(d.path) for d in library_dirs if d.path]) return schemas.Storage( total_storage=total_storage, used_storage=total_storage - free_storage @@ -75,6 +77,10 @@ def downloader(_: schemas.TokenPayload = Depends(verify_token)) -> Any: """ 查询下载器信息 """ + # 下载目录空间 + download_dirs = DirectoryHelper().get_download_dirs() + _, free_space = SystemUtils.space_usage([Path(d.path) for d in download_dirs if d.path]) + # 下载器信息 downloader_info = schemas.DownloaderInfo() transfer_infos = DashboardChain().downloader_info() if transfer_infos: @@ -83,7 +89,7 @@ def downloader(_: schemas.TokenPayload = Depends(verify_token)) -> Any: downloader_info.upload_speed += transfer_info.upload_speed downloader_info.download_size += transfer_info.download_size downloader_info.upload_size += transfer_info.upload_size - downloader_info.free_space = SystemUtils.free_space(settings.SAVE_PATH) + downloader_info.free_space = free_space return downloader_info diff --git a/app/api/servarr.py b/app/api/servarr.py index 73768c60..b1216ac3 100644 --- a/app/api/servarr.py +++ b/app/api/servarr.py @@ -6,7 +6,6 @@ from sqlalchemy.orm import Session from app import schemas from app.chain.media import MediaChain from app.chain.subscribe import SubscribeChain -from app.core.config import settings from app.core.metainfo import MetaInfo from app.core.security import verify_uri_apikey from app.db import get_db @@ -121,7 +120,7 @@ def arr_rootfolder(_: str = Depends(verify_uri_apikey)) -> Any: return [ { "id": 1, - "path": "/" if not settings.LIBRARY_PATHS else str(settings.LIBRARY_PATHS[0]), + "path": "/", "accessible": True, "freeSpace": 0, "unmappedFolders": [] diff --git a/app/chain/download.py b/app/chain/download.py index 86a81f09..8f254455 100644 --- a/app/chain/download.py +++ b/app/chain/download.py @@ -14,6 +14,8 @@ from app.core.meta import MetaBase from app.core.metainfo import MetaInfo from app.db.downloadhistory_oper import DownloadHistoryOper from app.db.mediaserver_oper import MediaServerOper +from app.helper.directory import DirectoryHelper +from app.helper.message import MessageHelper from app.helper.torrent import TorrentHelper from app.log import logger from app.schemas import ExistMediaInfo, NotExistMediaInfo, DownloadingTorrent, Notification @@ -32,6 +34,8 @@ class DownloadChain(ChainBase): self.torrent = TorrentHelper() self.downloadhis = DownloadHistoryOper() self.mediaserver = MediaServerOper() + self.directoryhelper = DirectoryHelper() + self.messagehelper = MessageHelper() def post_download_message(self, meta: MetaBase, mediainfo: MediaInfo, torrent: TorrentInfo, channel: MessageChannel = None, userid: str = None, username: str = None, @@ -227,35 +231,23 @@ class DownloadChain(ChainBase): # 下载目录 if not save_path: - if settings.DOWNLOAD_CATEGORY and _media and _media.category: + # 获取下载目录 + dir_info = self.directoryhelper.get_download_dir(_media) + if not dir_info or not dir_info.path: + logger.error(f"未找到下载目录:{_media.type.value} {_media.title_year}") + self.messagehelper.put(f"{_media.type.value} {_media.title_year} 未找到下载目录!", + title="下载失败", role="system") + return None + + if (dir_info.category or dir_info.auto_category) and _media and _media.category: # 开启下载二级目录 - if _media.type != MediaType.TV: - # 电影 - download_dir = settings.SAVE_MOVIE_PATH / _media.category - else: - if _media.genre_ids \ - and set(_media.genre_ids).intersection(set(settings.ANIME_GENREIDS)): - # 动漫 - download_dir = settings.SAVE_ANIME_PATH / _media.category - else: - # 电视剧 - download_dir = settings.SAVE_TV_PATH / _media.category + download_dir = Path(dir_info.path) / _media.category elif _media: # 未开启下载二级目录 - if _media.type != MediaType.TV: - # 电影 - download_dir = settings.SAVE_MOVIE_PATH - else: - if _media.genre_ids \ - and set(_media.genre_ids).intersection(set(settings.ANIME_GENREIDS)): - # 动漫 - download_dir = settings.SAVE_ANIME_PATH - else: - # 电视剧 - download_dir = settings.SAVE_TV_PATH + download_dir = Path(dir_info.path) else: # 未识别 - download_dir = settings.SAVE_PATH + download_dir = Path(dir_info.path) else: # 自定义下载目录 download_dir = Path(save_path) diff --git a/app/chain/media.py b/app/chain/media.py index 08a1596f..a735decc 100644 --- a/app/chain/media.py +++ b/app/chain/media.py @@ -34,7 +34,7 @@ class MediaChain(ChainBase, metaclass=Singleton): # 识别媒体信息 mediainfo: MediaInfo = self.recognize_media(meta=metainfo) if not mediainfo: - # 偿试使用辅助识别,如果有注册响应事件的话 + # 尝试使用辅助识别,如果有注册响应事件的话 if eventmanager.check(EventType.NameRecognize): logger.info(f'请求辅助识别,标题:{title} ...') mediainfo = self.recognize_help(title=title, org_meta=metainfo) @@ -143,7 +143,7 @@ class MediaChain(ChainBase, metaclass=Singleton): # 识别媒体信息 mediainfo = self.recognize_media(meta=file_meta) if not mediainfo: - # 偿试使用辅助识别,如果有注册响应事件的话 + # 尝试使用辅助识别,如果有注册响应事件的话 if eventmanager.check(EventType.NameRecognize): logger.info(f'请求辅助识别,标题:{file_path.name} ...') mediainfo = self.recognize_help(title=path, org_meta=file_meta) diff --git a/app/chain/subscribe.py b/app/chain/subscribe.py index d216b3fc..cd1ffb4f 100644 --- a/app/chain/subscribe.py +++ b/app/chain/subscribe.py @@ -600,11 +600,11 @@ class SubscribeChain(ChainBase): # 先判断是否有没识别的种子 if not torrent_mediainfo or (not torrent_mediainfo.tmdb_id and not torrent_mediainfo.douban_id): - logger.info(f'{torrent_info.site_name} - {torrent_info.title} 订阅缓存为未识别状态,偿试重新识别...') + logger.info(f'{torrent_info.site_name} - {torrent_info.title} 订阅缓存为未识别状态,尝试重新识别...') # 重新识别(不使用缓存) torrent_mediainfo = self.recognize_media(meta=torrent_meta, cache=False) if not torrent_mediainfo: - logger.warn(f'{torrent_info.site_name} - {torrent_info.title} 重新识别失败,偿试通过标题匹配...') + logger.warn(f'{torrent_info.site_name} - {torrent_info.title} 重新识别失败,尝试通过标题匹配...') if self.torrenthelper.match_torrent(mediainfo=mediainfo, torrent_meta=torrent_meta, torrent=torrent_info): diff --git a/app/chain/transfer.py b/app/chain/transfer.py index ad5d18da..ba588d5c 100644 --- a/app/chain/transfer.py +++ b/app/chain/transfer.py @@ -357,7 +357,7 @@ class TransferChain(ChainBase): transferinfo=transferinfo ) # 刮削单个文件 - if settings.SCRAP_METADATA: + if transferinfo.need_scrape: self.scrape_metadata(path=transferinfo.target_path, mediainfo=file_mediainfo, transfer_type=transfer_type, diff --git a/app/core/config.py b/app/core/config.py index 1de8d01c..81ff48d9 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -2,7 +2,7 @@ import secrets import sys import threading from pathlib import Path -from typing import List, Optional +from typing import Optional from pydantic import BaseSettings, validator @@ -55,8 +55,6 @@ class Settings(BaseSettings): RECOGNIZE_SOURCE: str = "themoviedb" # 刮削来源 themoviedb/douban SCRAP_SOURCE: str = "themoviedb" - # 刮削入库的媒体文件 - SCRAP_METADATA: bool = True # 新增已入库媒体是否跟随TMDB信息变化 SCRAP_FOLLOW_TMDB: bool = True # TMDB图片地址 @@ -159,16 +157,6 @@ class Settings(BaseSettings): TR_PASSWORD: Optional[str] = None # 种子标签 TORRENT_TAG: str = "MOVIEPILOT" - # 下载保存目录,容器内映射路径需要一致 - DOWNLOAD_PATH: Optional[str] = None - # 电影下载保存目录,容器内映射路径需要一致 - DOWNLOAD_MOVIE_PATH: Optional[str] = None - # 电视剧下载保存目录,容器内映射路径需要一致 - DOWNLOAD_TV_PATH: Optional[str] = None - # 动漫下载保存目录,容器内映射路径需要一致 - DOWNLOAD_ANIME_PATH: Optional[str] = None - # 下载目录二级分类 - DOWNLOAD_CATEGORY: bool = False # 下载站点字幕 DOWNLOAD_SUBTITLE: bool = True # 媒体服务器 emby/jellyfin/plex,多个媒体服务器,分割 @@ -211,16 +199,6 @@ class Settings(BaseSettings): OCR_HOST: str = "https://movie-pilot.org" # CookieCloud对应的浏览器UA USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.57" - # 媒体库目录,多个目录使用,分隔 - LIBRARY_PATH: Optional[str] = None - # 电影媒体库目录名 - LIBRARY_MOVIE_NAME: str = "电影" - # 电视剧媒体库目录名 - LIBRARY_TV_NAME: str = "电视剧" - # 动漫媒体库目录名,不设置时使用电视剧目录 - LIBRARY_ANIME_NAME: Optional[str] = None - # 二级分类 - LIBRARY_CATEGORY: bool = True # 电视剧动漫的分类genre_ids ANIME_GENREIDS = [16] # 电影重命名格式 @@ -255,6 +233,29 @@ class Settings(BaseSettings): # 服务器地址,对应 https://github.com/jxxghp/MoviePilot-Server 项目 MP_SERVER_HOST: str = "https://movie-pilot.org" + # 【已弃用】刮削入库的媒体文件 + SCRAP_METADATA: bool = True + # 【已弃用】下载保存目录,容器内映射路径需要一致 + DOWNLOAD_PATH: Optional[str] = None + # 【已弃用】电影下载保存目录,容器内映射路径需要一致 + DOWNLOAD_MOVIE_PATH: Optional[str] = None + # 【已弃用】电视剧下载保存目录,容器内映射路径需要一致 + DOWNLOAD_TV_PATH: Optional[str] = None + # 【已弃用】动漫下载保存目录,容器内映射路径需要一致 + DOWNLOAD_ANIME_PATH: Optional[str] = None + # 【已弃用】下载目录二级分类 + DOWNLOAD_CATEGORY: bool = False + # 【已弃用】媒体库目录,多个目录使用,分隔 + LIBRARY_PATH: Optional[str] = None + # 【已弃用】电影媒体库目录名 + LIBRARY_MOVIE_NAME: str = "电影" + # 【已弃用】电视剧媒体库目录名 + LIBRARY_TV_NAME: str = "电视剧" + # 【已弃用】动漫媒体库目录名,不设置时使用电视剧目录 + LIBRARY_ANIME_NAME: Optional[str] = None + # 【已弃用】二级分类 + LIBRARY_CATEGORY: bool = True + @validator("SUBSCRIBE_RSS_INTERVAL", "COOKIECLOUD_INTERVAL", "MEDIASERVER_SYNC_INTERVAL", @@ -338,48 +339,6 @@ class Settings(BaseSettings): "server": self.PROXY_HOST } - @property - def LIBRARY_PATHS(self) -> List[Path]: - if self.LIBRARY_PATH: - return [Path(path) for path in self.LIBRARY_PATH.split(",")] - return [self.CONFIG_PATH / "library"] - - @property - def SAVE_PATH(self) -> Path: - """ - 获取下载保存目录 - """ - if self.DOWNLOAD_PATH: - return Path(self.DOWNLOAD_PATH) - return self.CONFIG_PATH / "downloads" - - @property - def SAVE_MOVIE_PATH(self) -> Path: - """ - 获取电影下载保存目录 - """ - if self.DOWNLOAD_MOVIE_PATH: - return Path(self.DOWNLOAD_MOVIE_PATH) - return self.SAVE_PATH - - @property - def SAVE_TV_PATH(self) -> Path: - """ - 获取电视剧下载保存目录 - """ - if self.DOWNLOAD_TV_PATH: - return Path(self.DOWNLOAD_TV_PATH) - return self.SAVE_PATH - - @property - def SAVE_ANIME_PATH(self) -> Path: - """ - 获取动漫下载保存目录 - """ - if self.DOWNLOAD_ANIME_PATH: - return Path(self.DOWNLOAD_ANIME_PATH) - return self.SAVE_TV_PATH - @property def GITHUB_HEADERS(self): """ diff --git a/app/core/meta/metavideo.py b/app/core/meta/metavideo.py index ab511075..f2fffa26 100644 --- a/app/core/meta/metavideo.py +++ b/app/core/meta/metavideo.py @@ -1,5 +1,4 @@ import re -from pathlib import Path from typing import Optional from Pinyin2Hanzi import is_pinyin @@ -138,7 +137,7 @@ class MetaVideo(MetaBase): # 处理part if self.part and self.part.upper() == "PART": self.part = None - # 没有中文标题时,偿试中描述中获取中文名 + # 没有中文标题时,尝试中描述中获取中文名 if not self.cn_name and self.en_name and self.subtitle: if self.__is_pinyin(self.en_name): # 英文名是拼音 diff --git a/app/core/metainfo.py b/app/core/metainfo.py index fe28e4f8..ef25fa20 100644 --- a/app/core/metainfo.py +++ b/app/core/metainfo.py @@ -1,4 +1,3 @@ -import logging from pathlib import Path from typing import Tuple diff --git a/app/db/models/__init__.py b/app/db/models/__init__.py index 661f1107..235d3b06 100644 --- a/app/db/models/__init__.py +++ b/app/db/models/__init__.py @@ -7,4 +7,4 @@ from .subscribe import Subscribe from .systemconfig import SystemConfig from .transferhistory import TransferHistory from .user import User -from .userconfig import UserConfig \ No newline at end of file +from .userconfig import UserConfig diff --git a/app/helper/directory.py b/app/helper/directory.py new file mode 100644 index 00000000..a36a5a9d --- /dev/null +++ b/app/helper/directory.py @@ -0,0 +1,93 @@ +from typing import List, Optional + +from app import schemas +from app.core.config import settings +from app.core.context import MediaInfo +from app.db.systemconfig_oper import SystemConfigOper +from app.schemas.types import SystemConfigKey, MediaType + + +class DirectoryHelper: + """ + 下载目录/媒体库目录帮助类 + """ + + def __init__(self): + self.systemconfig = SystemConfigOper() + + def get_download_dirs(self) -> List[schemas.MediaDirectory]: + """ + 获取下载目录 + """ + dir_conf: List[dict] = self.systemconfig.get(SystemConfigKey.DownloadDirectories) + if not dir_conf: + return [] + return [schemas.MediaDirectory(**d) for d in dir_conf] + + def get_library_dirs(self) -> List[schemas.MediaDirectory]: + """ + 获取媒体库目录 + """ + dir_conf: List[dict] = self.systemconfig.get(SystemConfigKey.LibraryDirectories) + if not dir_conf: + return [] + return [schemas.MediaDirectory(**d) for d in dir_conf] + + def get_download_dir(self, media: MediaInfo = None) -> Optional[schemas.MediaDirectory]: + """ + 根据媒体信息获取下载目录 + :param media: 媒体信息 + """ + media_dirs = self.get_download_dirs() + # 按照配置顺序查找(保存后的数据已经排序) + for media_dir in media_dirs: + # 没有媒体信息时,返回第一个类型为全部的目录 + if (not media or media.type == MediaType.UNKNOWN) and not media_dir.media_type: + return media_dir + # 目录类型为全部的,符合条件 + if not media_dir.media_type: + return media_dir + # 处理类型 + if media.genre_ids \ + and set(media.genre_ids).intersection(set(settings.ANIME_GENREIDS)): + media_type = "动漫" + else: + media_type = media.type.value + # 目录类型相等,目录类别为全部,符合条件 + if media_dir.media_type == media_type and not media_dir.category: + return media_dir + # 目录类型相等,目录类别相等,符合条件 + if media_dir.media_type == media_type and media_dir.category == media.category: + return media_dir + + return None + + def get_library_dir(self, media: MediaInfo = None) -> Optional[schemas.MediaDirectory]: + """ + 根据媒体信息获取媒体库目录 + :param media: 媒体信息 + """ + library_dirs = self.get_library_dirs() + # 按照配置顺序查找(保存后的数据已经排序) + for library_dir in library_dirs: + # 没有媒体信息时,返回第一个类型为全部的目录 + if (not media or media.type == MediaType.UNKNOWN) and not library_dir.media_type: + return library_dir + # 目录类型为全部的,符合条件 + if not library_dir.media_type: + return library_dir + # 处理类型 + if media.genre_ids \ + and set(media.genre_ids).intersection(set(settings.ANIME_GENREIDS)): + media_type = "动漫" + else: + media_type = media.type.value + # 目录类型相等,目录类别为全部,符合条件 + if library_dir.media_type == media_type and not library_dir.category: + return library_dir + # 目录类型相等,目录类别相等,符合条件 + if library_dir.media_type == media_type and library_dir.category == media.category: + return library_dir + + return None + diff --git a/app/modules/filetransfer/__init__.py b/app/modules/filetransfer/__init__.py index 143f9991..27094d59 100644 --- a/app/modules/filetransfer/__init__.py +++ b/app/modules/filetransfer/__init__.py @@ -9,17 +9,26 @@ from app.core.config import settings from app.core.context import MediaInfo from app.core.meta import MetaBase from app.core.metainfo import MetaInfo, MetaInfoPath +from app.helper.directory import DirectoryHelper +from app.helper.message import MessageHelper from app.log import logger from app.modules import _ModuleBase -from app.schemas import TransferInfo, ExistMediaInfo, TmdbEpisode +from app.schemas import TransferInfo, ExistMediaInfo, TmdbEpisode, MediaDirectory from app.schemas.types import MediaType -from app.utils.string import StringUtils from app.utils.system import SystemUtils lock = Lock() class FileTransferModule(_ModuleBase): + """ + 文件整理模块 + """ + + def __init__(self): + super().__init__() + self.directoryhelper = DirectoryHelper() + self.messagehelper = MessageHelper() def init_module(self) -> None: pass @@ -35,34 +44,37 @@ class FileTransferModule(_ModuleBase): """ 测试模块连接性 """ - if not settings.DOWNLOAD_PATH: - return False, "下载目录未设置" + directoryhelper = DirectoryHelper() # 检查下载目录 - download_paths: List[str] = [] - for path in [settings.DOWNLOAD_PATH, - settings.DOWNLOAD_MOVIE_PATH, - settings.DOWNLOAD_TV_PATH, - settings.DOWNLOAD_ANIME_PATH]: + download_paths = directoryhelper.get_download_dirs() + if not download_paths: + return False, "下载目录未设置" + for d_path in download_paths: + path = d_path.path if not path: - continue + return False, f"下载目录 {d_path.name} 对应路径未设置" download_path = Path(path) if not download_path.exists(): - return False, f"下载目录 {download_path} 不存在" - download_paths.append(path) - # 下载目录的设备ID - download_devids = [Path(path).stat().st_dev for path in download_paths] + return False, f"下载目录 {d_path.name} 对应路径 {path} 不存在" # 检查媒体库目录 - if not settings.LIBRARY_PATH: + libaray_dirs = directoryhelper.get_library_dirs() + if not libaray_dirs: return False, "媒体库目录未设置" # 比较媒体库目录的设备ID - for path in settings.LIBRARY_PATHS: + for l_path in libaray_dirs: + path = l_path.path + if not path: + return False, f"媒体库目录 {l_path.name} 对应路径未设置" library_path = Path(path) if not library_path.exists(): - return False, f"媒体库目录不存在:{library_path}" + return False, f"媒体库目录{l_path.name} 对应的路径 {path} 不存在" if settings.DOWNLOADER_MONITOR and settings.TRANSFER_TYPE == "link": - if library_path.stat().st_dev not in download_devids: - return False, f"媒体库目录 {library_path} " \ - f"与下载目录 {','.join(download_paths)} 不在同一设备,将无法硬链接" + for d_path in download_paths: + download_path = Path(d_path.path) + if l_path.media_type == d_path.media_type and l_path.category == d_path.category: + if library_path.stat().st_dev != download_path.stat().st_dev: + return False, f"媒体库目录 {library_path} " \ + f"与下载目录 {download_path} 不在同一设备,将无法硬链接" return True, "" def init_setting(self) -> Tuple[str, Union[str, bool]]: @@ -83,10 +95,16 @@ class FileTransferModule(_ModuleBase): """ # 获取目标路径 if not target: - # 未指定目的目录,根据源目录选择一个媒体库 - target = self.get_target_path(in_path=path) + # 未指定目的目录,选择一个媒体库 + dir_info = DirectoryHelper().get_library_dir(mediainfo) + if not dir_info or not dir_info.path: + logger.error(f"{mediainfo.type.value} {mediainfo.title_year} 未找到有效的媒体库目录,无法转移文件,源路径:{path}") + return TransferInfo(success=False, + path=path, + message="未找到有效的媒体库目录") # 拼装媒体库一、二级子目录 - target = self.__get_dest_dir(mediainfo=mediainfo, target_dir=target) + need_scrape = dir_info.scrape + target = self.__get_dest_dir(mediainfo=mediainfo, target_dir=dir_info) else: # 指定了目的目录 if target.is_file(): @@ -94,24 +112,18 @@ class FileTransferModule(_ModuleBase): return TransferInfo(success=False, path=path, message=f"{target} 不是有效目录") - # 只拼装二级子目录(不要一级目录) - target = self.__get_dest_dir(mediainfo=mediainfo, target_dir=target, typename_dir=False) - - if not target: - logger.error("未找到媒体库目录,无法转移文件") - return TransferInfo(success=False, - path=path, - message="未找到媒体库目录") - else: - logger.info(f"获取转移目标路径:{target}") + # FIXME 指定了目的目录时,拿不到是否需要刮削的配置了 + need_scrape = False + logger.info(f"获取转移目标路径:{target}") # 转移 return self.transfer_media(in_path=path, in_meta=meta, mediainfo=mediainfo, transfer_type=transfer_type, target_dir=target, - episodes_info=episodes_info) + episodes_info=episodes_info, + need_scrape=need_scrape) @staticmethod def __transfer_command(file_item: Path, target_file: Path, transfer_type: str) -> int: @@ -384,43 +396,17 @@ class FileTransferModule(_ModuleBase): over_flag=over_flag) @staticmethod - def __get_dest_dir(mediainfo: MediaInfo, target_dir: Path, typename_dir: bool = True) -> Path: + def __get_dest_dir(mediainfo: MediaInfo, target_dir: MediaDirectory) -> Path: """ 根据设置并装媒体库目录 :param mediainfo: 媒体信息 :target_dir: 媒体库根目录 :typename_dir: 是否加上类型目录 """ - if not target_dir: - return target_dir - - if mediainfo.type == MediaType.MOVIE: - # 电影 - if typename_dir: - # 目的目录加上类型和二级分类 - target_dir = target_dir / settings.LIBRARY_MOVIE_NAME / mediainfo.category - else: - # 目的目录加上二级分类 - target_dir = target_dir / mediainfo.category - - if mediainfo.type == MediaType.TV: - # 电视剧 - if mediainfo.genre_ids \ - and set(mediainfo.genre_ids).intersection(set(settings.ANIME_GENREIDS)): - # 动漫 - if typename_dir: - target_dir = target_dir / (settings.LIBRARY_ANIME_NAME - or settings.LIBRARY_TV_NAME) / mediainfo.category - else: - target_dir = target_dir / mediainfo.category - else: - # 电视剧 - if typename_dir: - target_dir = target_dir / settings.LIBRARY_TV_NAME / mediainfo.category - else: - target_dir = target_dir / mediainfo.category - - return target_dir + if target_dir.auto_category or target_dir.category: + return Path(target_dir.path) / mediainfo.category + else: + return Path(target_dir.path) def transfer_media(self, in_path: Path, @@ -428,7 +414,8 @@ class FileTransferModule(_ModuleBase): mediainfo: MediaInfo, transfer_type: str, target_dir: Path, - episodes_info: List[TmdbEpisode] = None + episodes_info: List[TmdbEpisode] = None, + need_scrape: bool = False ) -> TransferInfo: """ 识别并转移一个文件或者一个目录下的所有文件 @@ -438,6 +425,7 @@ class FileTransferModule(_ModuleBase): :param target_dir: 媒体库根目录 :param transfer_type: 文件转移方式 :param episodes_info: 当前季的全部集信息 + :param need_scrape: 是否需要刮削 :return: TransferInfo、错误信息 """ # 检查目录路径 @@ -490,7 +478,8 @@ class FileTransferModule(_ModuleBase): path=in_path, target_path=new_path, total_size=file_size, - is_bluray=bluray_flag) + is_bluray=bluray_flag, + need_scrape=need_scrape) else: # 转移单个文件 if mediainfo.type == MediaType.TV: @@ -589,7 +578,8 @@ class FileTransferModule(_ModuleBase): total_size=file_size, is_bluray=False, file_list=[str(in_path)], - file_list_new=[str(new_file)]) + file_list_new=[str(new_file)], + need_scrape=need_scrape) @staticmethod def __get_naming_dict(meta: MetaBase, mediainfo: MediaInfo, file_ext: str = None, @@ -689,86 +679,21 @@ class FileTransferModule(_ModuleBase): else: return Path(render_str) - @staticmethod - def get_library_path(path: Path): - """ - 根据文件路径查询其所在的媒体库目录,查询不到的返回输入目录 - """ - if not path: - return None - if not settings.LIBRARY_PATHS: - return path - # 目的路径,多路径以,分隔 - dest_paths = settings.LIBRARY_PATHS - for libpath in dest_paths: - try: - if path.is_relative_to(libpath): - return libpath - except Exception as e: - logger.debug(f"计算媒体库路径时出错:{str(e)}") - continue - return path - - @staticmethod - def get_target_path(in_path: Path = None) -> Optional[Path]: - """ - 计算一个最好的目的目录,有in_path时找与in_path同路径的,没有in_path时,顺序查找1个符合大小要求的,没有in_path和size时,返回第1个 - :param in_path: 源目录 - """ - if not settings.LIBRARY_PATHS: - return None - # 目的路径,多路径以,分隔 - dest_paths = settings.LIBRARY_PATHS - # 只有一个路径,直接返回 - if len(dest_paths) == 1: - return dest_paths[0] - # 匹配有最长共同上级路径的目录 - max_length = 0 - target_path = None - if in_path: - for path in dest_paths: - try: - # 计算in_path和path的公共字符串长度 - relative = StringUtils.find_common_prefix(str(in_path), str(path)) - if len(str(path)) == len(relative): - # 目录完整匹配的,直接返回 - return path - if len(relative) > max_length: - # 更新最大长度 - max_length = len(relative) - target_path = path - except Exception as e: - logger.debug(f"计算目标路径时出错:{str(e)}") - continue - if target_path: - return target_path - # 顺序匹配第1个满足空间存储要求的目录 - if in_path.exists(): - file_size = in_path.stat().st_size - for path in dest_paths: - if SystemUtils.free_space(path) > file_size: - return path - # 默认返回第1个 - return dest_paths[0] - def media_exists(self, mediainfo: MediaInfo, **kwargs) -> Optional[ExistMediaInfo]: """ 判断媒体文件是否存在于本地文件系统,只支持标准媒体库结构 :param mediainfo: 识别的媒体信息 :return: 如不存在返回None,存在时返回信息,包括每季已存在所有集{type: movie/tv, seasons: {season: [episodes]}} """ - if not settings.LIBRARY_PATHS: - return None # 目的路径 - dest_paths = settings.LIBRARY_PATHS + dest_paths = DirectoryHelper().get_library_dirs() # 检查每一个媒体库目录 for dest_path in dest_paths: # 媒体库路径 - target_dir = self.get_target_path(dest_path) - if not target_dir: + if not dest_path.path: continue # 媒体分类路径 - target_dir = self.__get_dest_dir(mediainfo=mediainfo, target_dir=target_dir) + target_dir = self.__get_dest_dir(mediainfo=mediainfo, target_dir=dest_path) # 重命名格式 rename_format = settings.TV_RENAME_FORMAT \ if mediainfo.type == MediaType.TV else settings.MOVIE_RENAME_FORMAT diff --git a/app/modules/qbittorrent/__init__.py b/app/modules/qbittorrent/__init__.py index 20e04969..28203da8 100644 --- a/app/modules/qbittorrent/__init__.py +++ b/app/modules/qbittorrent/__init__.py @@ -192,7 +192,7 @@ class QbittorrentModule(_ModuleBase): if content_path: torrent_path = Path(content_path) else: - torrent_path = settings.SAVE_PATH / torrent.get('name') + torrent_path = torrent.get('save_path') / torrent.get('name') ret_torrents.append(TransferTorrent( title=torrent.get('name'), path=torrent_path, @@ -211,7 +211,7 @@ class QbittorrentModule(_ModuleBase): if content_path: torrent_path = Path(content_path) else: - torrent_path = settings.SAVE_PATH / torrent.get('name') + torrent_path = torrent.get('save_path') / torrent.get('name') ret_torrents.append(TransferTorrent( title=torrent.get('name'), path=torrent_path, diff --git a/app/scheduler.py b/app/scheduler.py index 77580f56..8419a013 100644 --- a/app/scheduler.py +++ b/app/scheduler.py @@ -76,10 +76,10 @@ class Scheduler(metaclass=Singleton): __max_try__ = 30 if self._auth_count > __max_try__: SchedulerChain().messagehelper.put(title=f"用户认证失败", - message="用户认证失败次数过多,将不再偿试认证!", + message="用户认证失败次数过多,将不再尝试认证!", role="system") return - logger.info("用户未认证,正在偿试重新认证...") + logger.info("用户未认证,正在尝试重新认证...") status, msg = SitesHelper().check_user() if status: self._auth_count = 0 @@ -95,7 +95,7 @@ class Scheduler(metaclass=Singleton): self._auth_count += 1 logger.error(f"用户认证失败:{msg},共失败 {self._auth_count} 次") if self._auth_count >= __max_try__: - logger.error("用户认证失败次数过多,将不再偿试认证!") + logger.error("用户认证失败次数过多,将不再尝试认证!") # 各服务的运行状态 self._jobs = { diff --git a/app/schemas/filetransfer.py b/app/schemas/filetransfer.py index 6d647604..415bc7de 100644 --- a/app/schemas/filetransfer.py +++ b/app/schemas/filetransfer.py @@ -18,7 +18,7 @@ class MediaDirectory(BaseModel): # 媒体类别 动画电影/国产剧 category: Optional[str] = None # 刮削媒体信息 - scrape: Optional[bool] = True + scrape: Optional[bool] = False # 自动二级分类,未指定类别时自动分类 auto_category: Optional[bool] = False # 优先级 diff --git a/app/schemas/transfer.py b/app/schemas/transfer.py index 370636d7..6fb5e4d4 100644 --- a/app/schemas/transfer.py +++ b/app/schemas/transfer.py @@ -59,6 +59,8 @@ class TransferInfo(BaseModel): fail_list: Optional[list] = [] # 错误信息 message: Optional[str] = None + # 是否需要刮削 + need_scrape: Optional[bool] = False def to_dict(self): """ diff --git a/database/versions/a40261701909_1_0_20.py b/database/versions/a40261701909_1_0_20.py new file mode 100644 index 00000000..f594cc4d --- /dev/null +++ b/database/versions/a40261701909_1_0_20.py @@ -0,0 +1,140 @@ +"""1.0.20 + +Revision ID: a40261701909 +Revises: ae9d8ed8df97 +Create Date: 2024-05-22 19:16:21.374806 + +""" +import json +from pathlib import Path + +from alembic import op +import sqlalchemy as sa + +from app.core.config import Settings + +# revision identifiers, used by Alembic. +revision = 'a40261701909' +down_revision = 'ae9d8ed8df97' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + """ + 升级目录配置 + """ + # 实例化配置 + _settings = Settings( + _env_file=Settings().CONFIG_PATH / "app.env", + _env_file_encoding="utf-8" + ) + # 下载目录配置升级 + download_dirs = [] + if _settings.DOWNLOAD_MOVIE_PATH: + download_dirs.append({ + "type": "download", + "name": "电影目录", + "path": _settings.DOWNLOAD_MOVIE_PATH, + "media_type": "电影", + "category": "", + "auto_category": True if _settings.DOWNLOAD_CATEGORY else False, + "priority": 1 + }) + if _settings.DOWNLOAD_TV_PATH: + download_dirs.append({ + "type": "download", + "name": "电视剧目录", + "path": _settings.DOWNLOAD_TV_PATH, + "media_type": "电视剧", + "category": "", + "auto_category": True if _settings.DOWNLOAD_CATEGORY else False, + "priority": 2 + }) + if _settings.DOWNLOAD_ANIME_PATH: + download_dirs.append({ + "type": "download", + "name": "动漫目录", + "path": _settings.DOWNLOAD_ANIME_PATH, + "media_type": "动漫", + "category": "", + "auto_category": True if _settings.DOWNLOAD_CATEGORY else False, + "priority": 3 + }) + if _settings.DOWNLOAD_PATH: + download_dirs.append({ + "type": "download", + "name": "下载目录", + "path": _settings.DOWNLOAD_PATH, + "media_type": "", + "category": "", + "auto_category": True if _settings.DOWNLOAD_CATEGORY else False, + "priority": 4 + }) + + # 插入数据库,报错的话则更新 + if download_dirs: + download_dirs_value = json.dumps(download_dirs) + try: + op.execute(f"INSERT INTO systemconfig (key, value) VALUES ('DownloadDirectories', '{download_dirs_value}');") + except Exception as e: + op.execute(f"UPDATE systemconfig SET value = '{download_dirs_value}' WHERE key = 'DownloadDirectories';") + + # 媒体库目录配置升级 + library_dirs = [] + if _settings.LIBRARY_PATH: + for library_path in _settings.LIBRARY_PATH.split(","): + if _settings.LIBRARY_MOVIE_NAME: + library_dirs.append({ + "type": "library", + "name": "电影目录", + "path": str(Path(library_path) / _settings.LIBRARY_MOVIE_NAME), + "media_type": "电影", + "category": "", + "auto_category": True if _settings.LIBRARY_CATEGORY else False, + "scrape": True if _settings.SCRAP_METADATA else False, + "priority": 1 + }) + if _settings.LIBRARY_TV_NAME: + library_dirs.append({ + "type": "library", + "name": "电视剧目录", + "path": str(Path(library_path) / _settings.LIBRARY_TV_NAME), + "media_type": "电视剧", + "category": "", + "auto_category": True if _settings.LIBRARY_CATEGORY else False, + "scrape": True if _settings.SCRAP_METADATA else False, + "priority": 2 + }) + if _settings.LIBRARY_ANIME_NAME: + library_dirs.append({ + "type": "library", + "name": "动漫目录", + "path": str(Path(library_path) / _settings.LIBRARY_ANIME_NAME), + "media_type": "动漫", + "category": "", + "auto_category": True if _settings.LIBRARY_CATEGORY else False, + "scrape": True if _settings.SCRAP_METADATA else False, + "priority": 3 + }) + library_dirs.append({ + "type": "library", + "name": "媒体库目录", + "path": library_path, + "media_type": "", + "category": "", + "auto_category": True if _settings.LIBRARY_CATEGORY else False, + "scrape": True if _settings.SCRAP_METADATA else False, + "priority": 4 + }) + # 插入数据库,报错的话则更新 + if library_dirs: + library_dirs_value = json.dumps(library_dirs) + try: + op.execute(f"INSERT INTO systemconfig (key, value) VALUES ('LibraryDirectories', '{library_dirs_value}');") + except Exception as e: + op.execute(f"UPDATE systemconfig SET value = '{library_dirs_value}' WHERE key = 'LibraryDirectories';") + + +def downgrade() -> None: + pass