feat:目录结构重大调整,谨慎更新到dev
This commit is contained in:
@ -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
|
||||
|
||||
|
||||
|
@ -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": []
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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):
|
||||
# 英文名是拼音
|
||||
|
@ -1,4 +1,3 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
|
||||
|
@ -7,4 +7,4 @@ from .subscribe import Subscribe
|
||||
from .systemconfig import SystemConfig
|
||||
from .transferhistory import TransferHistory
|
||||
from .user import User
|
||||
from .userconfig import UserConfig
|
||||
from .userconfig import UserConfig
|
||||
|
93
app/helper/directory.py
Normal file
93
app/helper/directory.py
Normal file
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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 = {
|
||||
|
@ -18,7 +18,7 @@ class MediaDirectory(BaseModel):
|
||||
# 媒体类别 动画电影/国产剧
|
||||
category: Optional[str] = None
|
||||
# 刮削媒体信息
|
||||
scrape: Optional[bool] = True
|
||||
scrape: Optional[bool] = False
|
||||
# 自动二级分类,未指定类别时自动分类
|
||||
auto_category: Optional[bool] = False
|
||||
# 优先级
|
||||
|
@ -59,6 +59,8 @@ class TransferInfo(BaseModel):
|
||||
fail_list: Optional[list] = []
|
||||
# 错误信息
|
||||
message: Optional[str] = None
|
||||
# 是否需要刮削
|
||||
need_scrape: Optional[bool] = False
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
|
140
database/versions/a40261701909_1_0_20.py
Normal file
140
database/versions/a40261701909_1_0_20.py
Normal file
@ -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
|
Reference in New Issue
Block a user