mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-13 07:26:45 +00:00
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb4981adb3 | ||
|
|
6880b42a84 | ||
|
|
97054adc61 | ||
|
|
de94e5d595 | ||
|
|
a5a734d091 | ||
|
|
efb607d22f | ||
|
|
d0b2787a7c | ||
|
|
d5988ff443 | ||
|
|
96b4f1b575 | ||
|
|
bb6b8439c7 | ||
|
|
9cdce4509d | ||
|
|
3956ab1fe8 | ||
|
|
14686fdb03 | ||
|
|
32892ab747 | ||
|
|
79c637e003 | ||
|
|
d7c260715a | ||
|
|
2dfb089a39 | ||
|
|
e04179525b | ||
|
|
d044364c68 | ||
|
|
a0f912ffbe | ||
|
|
d7c8b08d7a | ||
|
|
f752082e1b | ||
|
|
201ec21adf | ||
|
|
57590323b2 | ||
|
|
4636c7ada7 | ||
|
|
4c86a4da5f | ||
|
|
8dc9acf071 | ||
|
|
abebae3664 | ||
|
|
4f7d8866a0 | ||
|
|
cceb22d729 | ||
|
|
89edbb93f5 | ||
|
|
4ffb406172 | ||
|
|
293e417865 | ||
|
|
510c20dc70 | ||
|
|
8e1810955b | ||
|
|
73f732fe1d | ||
|
|
d6f5160959 | ||
|
|
d64a7086dd | ||
|
|
825d9b768f | ||
|
|
f758a47f4f | ||
|
|
fc69d7e6c1 | ||
|
|
edc30266c8 | ||
|
|
665da9dad3 | ||
|
|
4048acf60e | ||
|
|
f116229ecc | ||
|
|
f6a2efb256 | ||
|
|
af3a50f7ea | ||
|
|
44a0e5b4a7 | ||
|
|
f40a1246ff | ||
|
|
dd890c410c | ||
|
|
8fd7f2c875 | ||
|
|
8c09b3482f | ||
|
|
0066247a2b | ||
|
|
c7926fc575 | ||
|
|
ac5b9fd4e5 | ||
|
|
42dc539df6 | ||
|
|
e60d785a11 | ||
|
|
33558d6197 | ||
|
|
46d2ffeb75 |
@@ -116,9 +116,6 @@ def scrape(fileitem: schemas.FileItem,
|
||||
if storage == "local":
|
||||
if not scrape_path.exists():
|
||||
return schemas.Response(success=False, message="刮削路径不存在")
|
||||
else:
|
||||
if not fileitem.fileid:
|
||||
return schemas.Response(success=False, message="刮削文件ID无效")
|
||||
# 手动刮削
|
||||
chain.scrape_metadata(fileitem=fileitem, meta=meta, mediainfo=mediainfo)
|
||||
return schemas.Response(success=True, message=f"{fileitem.path} 刮削完成")
|
||||
|
||||
@@ -331,6 +331,29 @@ def read_rss_sites(db: Session = Depends(get_db),
|
||||
return rss_sites
|
||||
|
||||
|
||||
@router.get("/auth", summary="查询认证站点", response_model=dict)
|
||||
def read_auth_sites(_: schemas.TokenPayload = Depends(verify_token)) -> dict:
|
||||
"""
|
||||
获取可认证站点列表
|
||||
"""
|
||||
return SitesHelper().get_authsites()
|
||||
|
||||
|
||||
@router.post("/auth", summary="用户站点认证", response_model=schemas.Response)
|
||||
def auth_site(
|
||||
auth_info: schemas.SiteAuth,
|
||||
_: User = Depends(get_current_active_superuser)
|
||||
) -> Any:
|
||||
"""
|
||||
用户站点认证
|
||||
"""
|
||||
if not auth_info or not auth_info.site or not auth_info.params:
|
||||
return schemas.Response(success=False, message="请输入认证站点和认证参数")
|
||||
status, msg = SitesHelper().check_user(auth_info.site, auth_info.params)
|
||||
SystemConfigOper().set(SystemConfigKey.UserSiteAuthParams, auth_info.dict())
|
||||
return schemas.Response(success=status, message=msg)
|
||||
|
||||
|
||||
@router.get("/{site_id}", summary="站点详情", response_model=schemas.Site)
|
||||
def read_site(
|
||||
site_id: int,
|
||||
|
||||
@@ -151,8 +151,6 @@ def rename(fileitem: schemas.FileItem,
|
||||
"""
|
||||
if not new_name:
|
||||
return schemas.Response(success=False, message="新名称为空")
|
||||
if fileitem.storage != 'local' and not fileitem.fileid:
|
||||
return schemas.Response(success=False, message="资源ID获取失败")
|
||||
result = StorageChain().rename_file(fileitem, new_name)
|
||||
if result:
|
||||
if recursive:
|
||||
|
||||
@@ -180,7 +180,7 @@ class DownloadChain(ChainBase):
|
||||
torrent_file, content, download_folder, files, error_msg = self.torrent.download_torrent(
|
||||
url=torrent_url,
|
||||
cookie=site_cookie,
|
||||
ua=torrent.site_ua,
|
||||
ua=torrent.site_ua or settings.USER_AGENT,
|
||||
proxy=torrent.site_proxy)
|
||||
|
||||
if isinstance(content, str):
|
||||
@@ -204,10 +204,10 @@ class DownloadChain(ChainBase):
|
||||
def download_single(self, context: Context, torrent_file: Path = None,
|
||||
episodes: Set[int] = None,
|
||||
channel: MessageChannel = None, source: str = None,
|
||||
downloader: str = None,
|
||||
save_path: str = None,
|
||||
userid: Union[str, int] = None,
|
||||
username: str = None,
|
||||
downloader: str = None,
|
||||
media_category: str = None) -> Optional[str]:
|
||||
"""
|
||||
下载及发送通知
|
||||
@@ -216,15 +216,16 @@ class DownloadChain(ChainBase):
|
||||
:param episodes: 需要下载的集数
|
||||
:param channel: 通知渠道
|
||||
:param source: 通知来源
|
||||
:param downloader: 下载器
|
||||
:param save_path: 保存路径
|
||||
:param userid: 用户ID
|
||||
:param username: 调用下载的用户名/插件名
|
||||
:param downloader: 下载器
|
||||
:param media_category: 自定义媒体类别
|
||||
"""
|
||||
_torrent = context.torrent_info
|
||||
_media = context.media_info
|
||||
_meta = context.meta_info
|
||||
_site_downloader = _torrent.site_downloader
|
||||
|
||||
# 补充完整的media数据
|
||||
if not _media.genre_ids:
|
||||
@@ -251,35 +252,31 @@ class DownloadChain(ChainBase):
|
||||
|
||||
# 下载目录
|
||||
if save_path:
|
||||
# 有自定义下载目录时,尝试匹配目录配置
|
||||
dir_info = self.directoryhelper.get_dir(_media, src_path=Path(save_path))
|
||||
# 下载目录使用自定义的
|
||||
download_dir = Path(save_path)
|
||||
else:
|
||||
# 根据媒体信息查询下载目录配置
|
||||
dir_info = self.directoryhelper.get_dir(_media)
|
||||
# 拼装子目录
|
||||
if dir_info:
|
||||
# 一级目录
|
||||
if not dir_info.media_type and dir_info.download_type_folder:
|
||||
# 一级自动分类
|
||||
download_dir = Path(dir_info.download_path) / _media.type.value
|
||||
else:
|
||||
# 一级不分类
|
||||
download_dir = Path(dir_info.download_path)
|
||||
|
||||
# 拼装子目录
|
||||
if dir_info:
|
||||
# 一级目录
|
||||
if not dir_info.media_type and dir_info.download_type_folder:
|
||||
# 一级自动分类
|
||||
download_dir = Path(dir_info.download_path) / _media.type.value
|
||||
# 二级目录
|
||||
if not dir_info.media_category and dir_info.download_category_folder and _media and _media.category:
|
||||
# 二级自动分类
|
||||
download_dir = download_dir / _media.category
|
||||
else:
|
||||
# 一级不分类
|
||||
download_dir = Path(dir_info.download_path)
|
||||
|
||||
# 二级目录
|
||||
if not dir_info.media_category and dir_info.download_category_folder and _media and _media.category:
|
||||
# 二级自动分类
|
||||
download_dir = download_dir / _media.category
|
||||
elif save_path:
|
||||
# 自定义下载目录
|
||||
download_dir = Path(save_path)
|
||||
else:
|
||||
# 未找到下载目录,且没有自定义下载目录
|
||||
logger.error(f"未找到下载目录:{_media.type.value} {_media.title_year}")
|
||||
self.messagehelper.put(f"{_media.type.value} {_media.title_year} 未找到下载目录!",
|
||||
title="下载失败", role="system")
|
||||
return None
|
||||
# 未找到下载目录,且没有自定义下载目录
|
||||
logger.error(f"未找到下载目录:{_media.type.value} {_media.title_year}")
|
||||
self.messagehelper.put(f"{_media.type.value} {_media.title_year} 未找到下载目录!",
|
||||
title="下载失败", role="system")
|
||||
return None
|
||||
|
||||
# 添加下载
|
||||
result: Optional[tuple] = self.download(content=content,
|
||||
@@ -287,7 +284,7 @@ class DownloadChain(ChainBase):
|
||||
episodes=episodes,
|
||||
download_dir=download_dir,
|
||||
category=_media.category,
|
||||
downloader=downloader)
|
||||
downloader=downloader or _site_downloader)
|
||||
if result:
|
||||
_downloader, _hash, error_msg = result
|
||||
else:
|
||||
@@ -335,7 +332,7 @@ class DownloadChain(ChainBase):
|
||||
continue
|
||||
# 只处理视频格式
|
||||
if not Path(file).suffix \
|
||||
or Path(file).suffix not in settings.RMT_MEDIAEXT:
|
||||
or Path(file).suffix.lower() not in settings.RMT_MEDIAEXT:
|
||||
continue
|
||||
files_to_add.append({
|
||||
"download_hash": _hash,
|
||||
@@ -386,7 +383,8 @@ class DownloadChain(ChainBase):
|
||||
source: str = None,
|
||||
userid: str = None,
|
||||
username: str = None,
|
||||
media_category: str = None
|
||||
media_category: str = None,
|
||||
downloader: str = None
|
||||
) -> Tuple[List[Context], Dict[Union[int, str], Dict[int, NotExistMediaInfo]]]:
|
||||
"""
|
||||
根据缺失数据,自动种子列表中组合择优下载
|
||||
@@ -398,6 +396,7 @@ class DownloadChain(ChainBase):
|
||||
:param userid: 用户ID
|
||||
:param username: 调用下载的用户名/插件名
|
||||
:param media_category: 自定义媒体类别
|
||||
:param downloader: 下载器
|
||||
:return: 已经下载的资源列表、剩余未下载到的剧集 no_exists[tmdb_id/douban_id] = {season: NotExistMediaInfo}
|
||||
"""
|
||||
# 已下载的项目
|
||||
@@ -469,7 +468,7 @@ class DownloadChain(ChainBase):
|
||||
logger.info(f"开始下载电影 {context.torrent_info.title} ...")
|
||||
if self.download_single(context, save_path=save_path, channel=channel,
|
||||
source=source, userid=userid, username=username,
|
||||
media_category=media_category):
|
||||
media_category=media_category, downloader=downloader):
|
||||
# 下载成功
|
||||
logger.info(f"{context.torrent_info.title} 添加下载成功")
|
||||
downloaded_list.append(context)
|
||||
@@ -554,7 +553,8 @@ class DownloadChain(ChainBase):
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
media_category=media_category
|
||||
media_category=media_category,
|
||||
downloader=downloader,
|
||||
)
|
||||
else:
|
||||
# 下载
|
||||
@@ -562,7 +562,8 @@ class DownloadChain(ChainBase):
|
||||
download_id = self.download_single(context, save_path=save_path,
|
||||
channel=channel, source=source,
|
||||
userid=userid, username=username,
|
||||
media_category=media_category)
|
||||
media_category=media_category,
|
||||
downloader=downloader)
|
||||
|
||||
if download_id:
|
||||
# 下载成功
|
||||
@@ -633,7 +634,8 @@ class DownloadChain(ChainBase):
|
||||
download_id = self.download_single(context, save_path=save_path,
|
||||
channel=channel, source=source,
|
||||
userid=userid, username=username,
|
||||
media_category=media_category)
|
||||
media_category=media_category,
|
||||
downloader=downloader)
|
||||
if download_id:
|
||||
# 下载成功
|
||||
logger.info(f"{meta.title} 添加下载成功")
|
||||
@@ -722,7 +724,8 @@ class DownloadChain(ChainBase):
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
media_category=media_category
|
||||
media_category=media_category,
|
||||
downloader=downloader
|
||||
)
|
||||
if not download_id:
|
||||
continue
|
||||
|
||||
@@ -339,9 +339,10 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
||||
return
|
||||
tmp_file = settings.TEMP_PATH / _path.name
|
||||
tmp_file.write_bytes(_content)
|
||||
logger.info(f"保存文件:【{_fileitem.storage}】{_path}")
|
||||
_fileitem.path = str(_path.parent)
|
||||
self.storagechain.upload_file(fileitem=_fileitem, path=tmp_file)
|
||||
item = self.storagechain.upload_file(fileitem=_fileitem, path=tmp_file)
|
||||
if item:
|
||||
logger.info(f"已保存文件:{Path(item.path) / item.name}")
|
||||
if tmp_file.exists():
|
||||
tmp_file.unlink()
|
||||
|
||||
@@ -376,6 +377,11 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
||||
if mediainfo.type == MediaType.MOVIE:
|
||||
# 电影
|
||||
if fileitem.type == "file":
|
||||
# 是否已存在
|
||||
nfo_path = filepath.with_suffix(".nfo")
|
||||
if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage, path=nfo_path):
|
||||
logger.debug(f"已存在nfo文件:{nfo_path}")
|
||||
return
|
||||
# 电影文件
|
||||
logger.info(f"正在生成电影nfo:{mediainfo.title_year} - {filepath.name}")
|
||||
movie_nfo = self.metadata_nfo(meta=meta, mediainfo=mediainfo)
|
||||
@@ -383,10 +389,8 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
||||
logger.warn(f"{filepath.name} nfo文件生成失败!")
|
||||
return
|
||||
# 保存或上传nfo文件到上级目录
|
||||
nfo_path = filepath.with_suffix(".nfo")
|
||||
if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage, path=nfo_path):
|
||||
logger.info(f"已存在nfo文件:{nfo_path}")
|
||||
return
|
||||
if not parent:
|
||||
parent = self.storagechain.get_parent_item(fileitem)
|
||||
__save_file(_fileitem=parent, _path=nfo_path, _content=movie_nfo)
|
||||
else:
|
||||
# 电影目录
|
||||
@@ -408,7 +412,7 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
||||
image_path = filepath / image_name
|
||||
if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage,
|
||||
path=image_path):
|
||||
logger.info(f"已存在图片文件:{image_path}")
|
||||
logger.debug(f"已存在图片文件:{image_path}")
|
||||
continue
|
||||
# 下载图片
|
||||
content = __download_image(_url=attr_value)
|
||||
@@ -418,7 +422,12 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
||||
else:
|
||||
# 电视剧
|
||||
if fileitem.type == "file":
|
||||
# 当前为集文件,重新识别季集
|
||||
# 是否已存在
|
||||
nfo_path = filepath.with_suffix(".nfo")
|
||||
if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage, path=nfo_path):
|
||||
logger.debug(f"已存在nfo文件:{nfo_path}")
|
||||
return
|
||||
# 重新识别季集
|
||||
file_meta = MetaInfoPath(filepath)
|
||||
if not file_meta.begin_episode:
|
||||
logger.warn(f"{filepath.name} 无法识别文件集数!")
|
||||
@@ -434,10 +443,8 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
||||
logger.warn(f"{filepath.name} nfo生成失败!")
|
||||
return
|
||||
# 保存或上传nfo文件到上级目录
|
||||
nfo_path = filepath.with_suffix(".nfo")
|
||||
if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage, path=nfo_path):
|
||||
logger.info(f"已存在nfo文件:{nfo_path}")
|
||||
return
|
||||
if not parent:
|
||||
parent = self.storagechain.get_parent_item(fileitem)
|
||||
__save_file(_fileitem=parent, _path=nfo_path, _content=episode_nfo)
|
||||
# 获取集的图片
|
||||
image_dict = self.metadata_img(mediainfo=file_mediainfo,
|
||||
@@ -446,12 +453,14 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
||||
for episode, image_url in image_dict.items():
|
||||
image_path = filepath.with_suffix(Path(image_url).suffix)
|
||||
if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage, path=image_path):
|
||||
logger.info(f"已存在图片文件:{image_path}")
|
||||
logger.debug(f"已存在图片文件:{image_path}")
|
||||
continue
|
||||
# 下载图片
|
||||
content = __download_image(image_url)
|
||||
# 保存图片文件到当前目录
|
||||
if content:
|
||||
if not parent:
|
||||
parent = self.storagechain.get_parent_item(fileitem)
|
||||
__save_file(_fileitem=parent, _path=image_path, _content=content)
|
||||
|
||||
else:
|
||||
@@ -467,16 +476,17 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
||||
# 识别文件夹名称
|
||||
season_meta = MetaInfo(filepath.name)
|
||||
if season_meta.begin_season:
|
||||
# 是否已存在
|
||||
nfo_path = filepath / "season.nfo"
|
||||
if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage, path=nfo_path):
|
||||
logger.debug(f"已存在nfo文件:{nfo_path}")
|
||||
return
|
||||
# 当前目录有季号,生成季nfo
|
||||
season_nfo = self.metadata_nfo(meta=meta, mediainfo=mediainfo, season=season_meta.begin_season)
|
||||
if not season_nfo:
|
||||
logger.warn(f"无法生成电视剧季nfo文件:{meta.name}")
|
||||
return
|
||||
# 写入nfo到根目录
|
||||
nfo_path = filepath / "season.nfo"
|
||||
if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage, path=nfo_path):
|
||||
logger.info(f"已存在nfo文件:{nfo_path}")
|
||||
return
|
||||
__save_file(_fileitem=fileitem, _path=nfo_path, _content=season_nfo)
|
||||
# TMDB季poster图片
|
||||
image_dict = self.metadata_img(mediainfo=mediainfo, season=season_meta.begin_season)
|
||||
@@ -485,7 +495,7 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
||||
image_path = filepath.with_name(image_name)
|
||||
if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage,
|
||||
path=image_path):
|
||||
logger.info(f"已存在图片文件:{image_path}")
|
||||
logger.debug(f"已存在图片文件:{image_path}")
|
||||
continue
|
||||
# 下载图片
|
||||
content = __download_image(image_url)
|
||||
@@ -494,16 +504,17 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
||||
__save_file(_fileitem=fileitem, _path=image_path, _content=content)
|
||||
# 判断当前目录是不是剧集根目录
|
||||
if season_meta.name:
|
||||
# 是否已存在
|
||||
nfo_path = filepath / "tvshow.nfo"
|
||||
if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage, path=nfo_path):
|
||||
logger.debug(f"已存在nfo文件:{nfo_path}")
|
||||
return
|
||||
# 当前目录有名称,生成tvshow nfo 和 tv图片
|
||||
tv_nfo = self.metadata_nfo(meta=meta, mediainfo=mediainfo)
|
||||
if not tv_nfo:
|
||||
logger.warn(f"无法生成电视剧nfo文件:{meta.name}")
|
||||
return
|
||||
# 写入tvshow nfo到根目录
|
||||
nfo_path = filepath / "tvshow.nfo"
|
||||
if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage, path=nfo_path):
|
||||
logger.info(f"已存在nfo文件:{nfo_path}")
|
||||
return
|
||||
__save_file(_fileitem=fileitem, _path=nfo_path, _content=tv_nfo)
|
||||
# 生成目录图片
|
||||
image_dict = self.metadata_img(mediainfo=mediainfo)
|
||||
@@ -512,7 +523,7 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
||||
image_path = filepath / image_name
|
||||
if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage,
|
||||
path=image_path):
|
||||
logger.info(f"已存在图片文件:{image_path}")
|
||||
logger.debug(f"已存在图片文件:{image_path}")
|
||||
continue
|
||||
# 下载图片
|
||||
content = __download_image(image_url)
|
||||
|
||||
@@ -3,12 +3,12 @@ from typing import List, Union, Optional, Generator
|
||||
|
||||
from cachetools import cached, TTLCache
|
||||
|
||||
from app import schemas
|
||||
from app.chain import ChainBase
|
||||
from app.core.config import global_vars
|
||||
from app.db.mediaserver_oper import MediaServerOper
|
||||
from app.helper.service import ServiceConfigHelper
|
||||
from app.log import logger
|
||||
from app.schemas import MediaServerLibrary, MediaServerItem, MediaServerSeasonInfo, MediaServerPlayItem
|
||||
|
||||
lock = threading.Lock()
|
||||
|
||||
@@ -22,7 +22,7 @@ class MediaServerChain(ChainBase):
|
||||
super().__init__()
|
||||
self.dboper = MediaServerOper()
|
||||
|
||||
def librarys(self, server: str, username: str = None, hidden: bool = False) -> List[schemas.MediaServerLibrary]:
|
||||
def librarys(self, server: str, username: str = None, hidden: bool = False) -> List[MediaServerLibrary]:
|
||||
"""
|
||||
获取媒体服务器所有媒体库
|
||||
"""
|
||||
@@ -70,25 +70,25 @@ class MediaServerChain(ChainBase):
|
||||
yield from self.run_module("mediaserver_items", server=server, library_id=library_id,
|
||||
start_index=start_index, limit=limit)
|
||||
|
||||
def iteminfo(self, server: str, item_id: Union[str, int]) -> schemas.MediaServerItem:
|
||||
def iteminfo(self, server: str, item_id: Union[str, int]) -> MediaServerItem:
|
||||
"""
|
||||
获取媒体服务器项目信息
|
||||
"""
|
||||
return self.run_module("mediaserver_iteminfo", server=server, item_id=item_id)
|
||||
|
||||
def episodes(self, server: str, item_id: Union[str, int]) -> List[schemas.MediaServerSeasonInfo]:
|
||||
def episodes(self, server: str, item_id: Union[str, int]) -> List[MediaServerSeasonInfo]:
|
||||
"""
|
||||
获取媒体服务器剧集信息
|
||||
"""
|
||||
return self.run_module("mediaserver_tv_episodes", server=server, item_id=item_id)
|
||||
|
||||
def playing(self, server: str, count: int = 20, username: str = None) -> List[schemas.MediaServerPlayItem]:
|
||||
def playing(self, server: str, count: int = 20, username: str = None) -> List[MediaServerPlayItem]:
|
||||
"""
|
||||
获取媒体服务器正在播放信息
|
||||
"""
|
||||
return self.run_module("mediaserver_playing", count=count, server=server, username=username)
|
||||
|
||||
def latest(self, server: str, count: int = 20, username: str = None) -> List[schemas.MediaServerPlayItem]:
|
||||
def latest(self, server: str, count: int = 20, username: str = None) -> List[MediaServerPlayItem]:
|
||||
"""
|
||||
获取媒体服务器最新入库条目
|
||||
"""
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import Optional, Tuple, List, Dict
|
||||
from app import schemas
|
||||
from app.chain import ChainBase
|
||||
from app.core.config import settings
|
||||
from app.helper.directory import DirectoryHelper
|
||||
from app.log import logger
|
||||
from app.schemas import MediaType
|
||||
|
||||
@@ -13,6 +14,10 @@ class StorageChain(ChainBase):
|
||||
存储处理链
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.directoryhelper = DirectoryHelper()
|
||||
|
||||
def save_config(self, storage: str, conf: dict) -> None:
|
||||
"""
|
||||
保存存储配置
|
||||
@@ -57,13 +62,15 @@ class StorageChain(ChainBase):
|
||||
"""
|
||||
return self.run_module("download_file", fileitem=fileitem, path=path)
|
||||
|
||||
def upload_file(self, fileitem: schemas.FileItem, path: Path) -> Optional[bool]:
|
||||
def upload_file(self, fileitem: schemas.FileItem, path: Path,
|
||||
new_name: str = None) -> Optional[schemas.FileItem]:
|
||||
"""
|
||||
上传文件
|
||||
:param fileitem: 保存目录项
|
||||
:param path: 本地文件路径
|
||||
:param new_name: 新文件名
|
||||
"""
|
||||
return self.run_module("upload_file", fileitem=fileitem, path=path)
|
||||
return self.run_module("upload_file", fileitem=fileitem, path=path, new_name=new_name)
|
||||
|
||||
def delete_file(self, fileitem: schemas.FileItem) -> Optional[bool]:
|
||||
"""
|
||||
@@ -107,26 +114,44 @@ class StorageChain(ChainBase):
|
||||
"""
|
||||
return self.run_module("support_transtype", storage=storage)
|
||||
|
||||
def delete_media_file(self, fileitem: schemas.FileItem, mtype: MediaType = None) -> bool:
|
||||
def delete_media_file(self, fileitem: schemas.FileItem,
|
||||
mtype: MediaType = None, delete_self: bool = True) -> bool:
|
||||
"""
|
||||
删除媒体文件,以及不含媒体文件的目录
|
||||
"""
|
||||
state = self.delete_file(fileitem)
|
||||
if not state:
|
||||
logger.warn(f"【{fileitem.storage}】{fileitem.path} 删除失败")
|
||||
media_exts = settings.RMT_MEDIAEXT + settings.DOWNLOAD_TMPEXT
|
||||
if fileitem.path == "/" or len(Path(fileitem.path).parts) <= 2:
|
||||
logger.warn(f"【{fileitem.storage}】{fileitem.path} 根目录或一级目录不允许删除")
|
||||
return False
|
||||
if fileitem.type == "dir":
|
||||
return True
|
||||
# 上级目录
|
||||
# 本身是目录
|
||||
if self.any_files(fileitem, extensions=media_exts) is False:
|
||||
logger.warn(f"【{fileitem.storage}】{fileitem.path} 不存在其它媒体文件,删除空目录")
|
||||
return self.delete_file(fileitem)
|
||||
return False
|
||||
elif delete_self:
|
||||
# 本身是文件
|
||||
logger.warn(f"正在删除【{fileitem.storage}】{fileitem.path}")
|
||||
if not self.delete_file(fileitem):
|
||||
logger.warn(f"【{fileitem.storage}】{fileitem.path} 删除失败")
|
||||
return False
|
||||
# 处理上级目录
|
||||
if mtype and mtype == MediaType.TV:
|
||||
dir_path = Path(fileitem.path).parent.parent
|
||||
dir_item = self.get_file_item(storage=fileitem.storage, path=dir_path)
|
||||
dir_item = self.get_file_item(storage=fileitem.storage, path=Path(fileitem.path).parent.parent)
|
||||
else:
|
||||
dir_item = self.get_parent_item(fileitem)
|
||||
if dir_item:
|
||||
if dir_item and len(Path(dir_item.path).parts) > 2:
|
||||
# 如何目录是所有下载目录、媒体库目录的上级,则不处理
|
||||
for d in self.directoryhelper.get_dirs():
|
||||
if d.download_path and Path(d.download_path).is_relative_to(Path(dir_item.path)):
|
||||
logger.debug(f"【{dir_item.storage}】{dir_item.path} 是下载目录本级或上级目录,不删除")
|
||||
return True
|
||||
if d.library_path and Path(d.library_path).is_relative_to(Path(dir_item.path)):
|
||||
logger.debug(f"【{dir_item.storage}】{dir_item.path} 是媒体库目录本级或上级目录,不删除")
|
||||
return True
|
||||
# 不存在其他媒体文件,删除空目录
|
||||
if not self.any_files(dir_item, extensions=settings.RMT_MEDIAEXT):
|
||||
if self.any_files(dir_item, extensions=media_exts) is False:
|
||||
logger.warn(f"【{dir_item.storage}】{dir_item.path} 不存在其它媒体文件,删除空目录")
|
||||
return self.delete_file(dir_item)
|
||||
|
||||
# 存在媒体文件,返回文件删除状态
|
||||
return state
|
||||
return True
|
||||
|
||||
@@ -159,6 +159,8 @@ class SubscribeChain(ChainBase):
|
||||
"search_imdbid") else kwargs.get("search_imdbid"),
|
||||
'sites': self.__get_default_subscribe_config(mediainfo.type, "sites") or None if not kwargs.get(
|
||||
"sites") else kwargs.get("sites"),
|
||||
'downloader': self.__get_default_subscribe_config(mediainfo.type, "downloader") if not kwargs.get(
|
||||
"downloader") else kwargs.get("downloader"),
|
||||
'save_path': self.__get_default_subscribe_config(mediainfo.type, "save_path") if not kwargs.get(
|
||||
"save_path") else kwargs.get("save_path")
|
||||
})
|
||||
@@ -363,10 +365,6 @@ class SubscribeChain(ChainBase):
|
||||
torrent_info = context.torrent_info
|
||||
torrent_mediainfo = context.media_info
|
||||
|
||||
# 匹配订阅附加参数
|
||||
if not self.torrenthelper.filter_torrent(torrent_info=torrent_info,
|
||||
filter_params=self.get_params(subscribe)):
|
||||
continue
|
||||
# 洗版
|
||||
if subscribe.best_version:
|
||||
# 洗版时,非整季不要
|
||||
@@ -394,7 +392,8 @@ class SubscribeChain(ChainBase):
|
||||
userid=subscribe.username,
|
||||
username=subscribe.username,
|
||||
save_path=subscribe.save_path,
|
||||
media_category=subscribe.media_category
|
||||
media_category=subscribe.media_category,
|
||||
downloader=subscribe.downloader,
|
||||
)
|
||||
|
||||
# 判断是否应完成订阅
|
||||
@@ -773,7 +772,8 @@ class SubscribeChain(ChainBase):
|
||||
userid=subscribe.username,
|
||||
username=subscribe.username,
|
||||
save_path=subscribe.save_path,
|
||||
media_category=subscribe.media_category)
|
||||
media_category=subscribe.media_category,
|
||||
downloader=subscribe.downloader)
|
||||
# 判断是否要完成订阅
|
||||
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo,
|
||||
downloads=downloads, lefts=lefts)
|
||||
@@ -1241,6 +1241,9 @@ class SubscribeChain(ChainBase):
|
||||
file_path=file.fullpath,
|
||||
)
|
||||
if subscribe.type == MediaType.TV.value:
|
||||
season_number = file_meta.begin_season
|
||||
if season_number and season_number != subscribe.season:
|
||||
continue
|
||||
episode_number = file_meta.begin_episode
|
||||
if episode_number and episodes.get(episode_number):
|
||||
episodes[episode_number].download.append(file_info)
|
||||
@@ -1278,6 +1281,9 @@ class SubscribeChain(ChainBase):
|
||||
file_path=fileitem.path,
|
||||
)
|
||||
if subscribe.type == MediaType.TV.value:
|
||||
season_number = file_meta.begin_season
|
||||
if season_number and season_number != subscribe.season:
|
||||
continue
|
||||
episode_number = file_meta.begin_episode
|
||||
if episode_number and episodes.get(episode_number):
|
||||
episodes[episode_number].library.append(file_info)
|
||||
|
||||
@@ -120,6 +120,7 @@ class TorrentsChain(ChainBase, metaclass=Singleton):
|
||||
site_ua=site.get("ua") or settings.USER_AGENT,
|
||||
site_proxy=site.get("proxy"),
|
||||
site_order=site.get("pri"),
|
||||
site_downloader=site.get("downloader"),
|
||||
title=item.get("title"),
|
||||
enclosure=item.get("enclosure"),
|
||||
page_url=item.get("link"),
|
||||
|
||||
@@ -389,14 +389,13 @@ class TransferChain(ChainBase):
|
||||
download_hash = download_file.download_hash
|
||||
|
||||
# 查询整理目标目录
|
||||
if not target_directory:
|
||||
if target_path:
|
||||
target_directory = self.directoryhelper.get_dir(file_mediainfo,
|
||||
storage=target_storage, dest_path=target_path)
|
||||
elif src_match:
|
||||
if not target_directory and not target_path:
|
||||
if src_match:
|
||||
# 按源目录匹配,以便找到更合适的目录配置
|
||||
target_directory = self.directoryhelper.get_dir(file_mediainfo,
|
||||
storage=file_item.storage, src_path=file_path)
|
||||
else:
|
||||
# 未指定目标路径,根据媒体信息获取目标目录
|
||||
target_directory = self.directoryhelper.get_dir(file_mediainfo)
|
||||
|
||||
# 执行整理
|
||||
@@ -514,13 +513,7 @@ class TransferChain(ChainBase):
|
||||
logger.info(f"移动模式删除种子成功:{download_hash} ")
|
||||
# 删除残留目录
|
||||
if fileitem:
|
||||
if fileitem.type == "dir":
|
||||
folder_item = fileitem
|
||||
else:
|
||||
folder_item = self.storagechain.get_parent_item(fileitem)
|
||||
if folder_item and not self.storagechain.any_files(folder_item, extensions=settings.RMT_MEDIAEXT):
|
||||
logger.warn(f"删除残留空文件夹:【{folder_item.storage}】{folder_item.path}")
|
||||
self.storagechain.delete_file(folder_item)
|
||||
self.storagechain.delete_media_file(fileitem, delete_self=False)
|
||||
|
||||
# 结束进度
|
||||
logger.info(f"{fileitem.path} 整理完成,共 {total_num} 个文件,"
|
||||
|
||||
@@ -136,7 +136,7 @@ class ConfigModel(BaseModel):
|
||||
'.aifc', '.aiff', '.alac', '.adif', '.adts',
|
||||
'.flac', '.midi', '.opus', '.sfalc']
|
||||
# 下载器临时文件后缀
|
||||
DOWNLOAD_TMPEXT: list = ['.!qB', '.part']
|
||||
DOWNLOAD_TMPEXT: list = ['.!qb', '.part']
|
||||
# 媒体服务器同步间隔(小时)
|
||||
MEDIASERVER_SYNC_INTERVAL: int = 6
|
||||
# 订阅模式
|
||||
|
||||
@@ -23,6 +23,8 @@ class TorrentInfo:
|
||||
site_proxy: bool = False
|
||||
# 站点优先级
|
||||
site_order: int = 0
|
||||
# 站点下载器
|
||||
site_downloader: str = None
|
||||
# 种子名称
|
||||
title: str = None
|
||||
# 种子副标题
|
||||
|
||||
@@ -84,6 +84,7 @@ class EventManager(metaclass=Singleton):
|
||||
self.__disabled_handlers = set() # 禁用的事件处理器集合
|
||||
self.__disabled_classes = set() # 禁用的事件处理器类集合
|
||||
self.__lock = threading.Lock() # 线程锁
|
||||
self.__processing_events = {} # 用于记录当前正在处理的事件 {event_hash: event}
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
@@ -129,6 +130,14 @@ class EventManager(metaclass=Singleton):
|
||||
for handler in handlers.values()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def __get_event_hash(event: Event) -> str:
|
||||
"""
|
||||
计算事件的唯一标识符(hash)
|
||||
"""
|
||||
data_string = str(event.event_type.value) + str(event.event_data)
|
||||
return str(uuid.uuid5(uuid.NAMESPACE_DNS, data_string))
|
||||
|
||||
def send_event(self, etype: Union[EventType, ChainEventType], data: Optional[Union[Dict, ChainEventData]] = None,
|
||||
priority: int = DEFAULT_EVENT_PRIORITY) -> Optional[Event]:
|
||||
"""
|
||||
@@ -139,6 +148,12 @@ class EventManager(metaclass=Singleton):
|
||||
:return: 如果是链式事件,返回处理后的事件数据;否则返回 None
|
||||
"""
|
||||
event = Event(etype, data, priority)
|
||||
event_hash = self.__get_event_hash(event)
|
||||
with self.__lock:
|
||||
if event_hash in self.__processing_events:
|
||||
logger.debug(f"Duplicate event ignored: {event}")
|
||||
return None
|
||||
self.__processing_events[event_hash] = event
|
||||
if isinstance(etype, EventType):
|
||||
self.__trigger_broadcast_event(event)
|
||||
elif isinstance(etype, ChainEventType):
|
||||
@@ -320,9 +335,14 @@ class EventManager(metaclass=Singleton):
|
||||
"""
|
||||
触发链式事件,按顺序调用订阅的处理器,并记录处理耗时
|
||||
"""
|
||||
logger.debug(f"Triggering synchronous chain event: {event}")
|
||||
dispatch = self.__dispatch_chain_event(event)
|
||||
return event if dispatch else None
|
||||
try:
|
||||
logger.debug(f"Triggering synchronous chain event: {event}")
|
||||
dispatch = self.__dispatch_chain_event(event)
|
||||
return event if dispatch else None
|
||||
finally:
|
||||
event_hash = self.__get_event_hash(event)
|
||||
with self.__lock:
|
||||
self.__processing_events.pop(event_hash, None)
|
||||
|
||||
def __trigger_broadcast_event(self, event: Event):
|
||||
"""
|
||||
@@ -363,6 +383,9 @@ class EventManager(metaclass=Singleton):
|
||||
return
|
||||
for handler_id, handler in handlers.items():
|
||||
self.__executor.submit(self.__safe_invoke_handler, handler, event)
|
||||
event_hash = self.__get_event_hash(event)
|
||||
with self.__lock:
|
||||
self.__processing_events.pop(event_hash, None)
|
||||
|
||||
def __safe_invoke_handler(self, handler: Callable, event: Event):
|
||||
"""
|
||||
@@ -499,11 +522,18 @@ class EventManager(metaclass=Singleton):
|
||||
def decorator(f: Callable):
|
||||
# 将输入的事件类型统一转换为列表格式
|
||||
if isinstance(etype, list):
|
||||
event_list = etype # 传入的已经是列表,直接使用
|
||||
# 传入的已经是列表,直接使用
|
||||
event_list = etype
|
||||
elif etype is EventType:
|
||||
# 订阅所有事件
|
||||
event_list = []
|
||||
for et in etype:
|
||||
event_list.append(et)
|
||||
else:
|
||||
event_list = [etype] # 不是列表则包裹成单一元素的列表
|
||||
# 不是列表则包裹成单一元素的列表
|
||||
event_list = [etype]
|
||||
|
||||
# 遍历列表,处理每个事件类型
|
||||
# 遍历列表,处理每个事件类型
|
||||
for event in event_list:
|
||||
if isinstance(event, (EventType, ChainEventType)):
|
||||
self.add_event_listener(event, f)
|
||||
|
||||
@@ -30,8 +30,8 @@ class MetaVideo(MetaBase):
|
||||
_episode_re = r"EP?(\d{2,4})$|^EP?(\d{1,4})$|^S\d{1,2}EP?(\d{1,4})$|S\d{2}EP?(\d{2,4})"
|
||||
_part_re = r"(^PART[0-9ABI]{0,2}$|^CD[0-9]{0,2}$|^DVD[0-9]{0,2}$|^DISK[0-9]{0,2}$|^DISC[0-9]{0,2}$)"
|
||||
_roman_numerals = r"^(?=[MDCLXVI])M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$"
|
||||
_source_re = r"^BLURAY$|^HDTV$|^UHDTV$|^HDDVD$|^WEBRIP$|^DVDRIP$|^BDRIP$|^BLU$|^WEB$|^BD$|^HDRip$|^REMUX$|^UHD$|^REPACK$"
|
||||
_effect_re = r"^SDR$|^HDR\d*$|^DOLBY$|^DOVI$|^DV$|^3D$"
|
||||
_source_re = r"^BLURAY$|^HDTV$|^UHDTV$|^HDDVD$|^WEBRIP$|^DVDRIP$|^BDRIP$|^BLU$|^WEB$|^BD$|^HDRip$|^REMUX$|^UHD$"
|
||||
_effect_re = r"^SDR$|^HDR\d*$|^DOLBY$|^DOVI$|^DV$|^3D$|^REPACK$"
|
||||
_resources_type_re = r"%s|%s" % (_source_re, _effect_re)
|
||||
_name_no_begin_re = r"^[\[【].+?[\]】]"
|
||||
_name_no_chinese_re = r".*版|.*字幕"
|
||||
@@ -524,16 +524,7 @@ class MetaVideo(MetaBase):
|
||||
"""
|
||||
if not self.name:
|
||||
return
|
||||
source_res = re.search(r"(%s)" % self._source_re, token, re.IGNORECASE)
|
||||
if source_res:
|
||||
self._last_token_type = "source"
|
||||
self._continue_flag = False
|
||||
self._stop_name_flag = True
|
||||
if not self._source:
|
||||
self._source = source_res.group(1)
|
||||
self._last_token = self._source.upper()
|
||||
return
|
||||
elif token.upper() == "DL" \
|
||||
if token.upper() == "DL" \
|
||||
and self._last_token_type == "source" \
|
||||
and self._last_token == "WEB":
|
||||
self._source = "WEB-DL"
|
||||
@@ -542,13 +533,37 @@ class MetaVideo(MetaBase):
|
||||
elif token.upper() == "RAY" \
|
||||
and self._last_token_type == "source" \
|
||||
and self._last_token == "BLU":
|
||||
self._source = "BluRay"
|
||||
# UHD BluRay组合
|
||||
if self._source == "UHD":
|
||||
self._source = "UHD BluRay"
|
||||
else:
|
||||
self._source = "BluRay"
|
||||
self._continue_flag = False
|
||||
return
|
||||
elif token.upper() == "WEBDL":
|
||||
self._source = "WEB-DL"
|
||||
self._continue_flag = False
|
||||
return
|
||||
# UHD REMUX组合
|
||||
if token.upper() == "REMUX" \
|
||||
and self._source == "BluRay":
|
||||
self._source = "BluRay REMUX"
|
||||
self._continue_flag = False
|
||||
return
|
||||
elif token.upper() == "BLURAY" \
|
||||
and self._source == "UHD":
|
||||
self._source = "UHD BluRay"
|
||||
self._continue_flag = False
|
||||
return
|
||||
source_res = re.search(r"(%s)" % self._source_re, token, re.IGNORECASE)
|
||||
if source_res:
|
||||
self._last_token_type = "source"
|
||||
self._continue_flag = False
|
||||
self._stop_name_flag = True
|
||||
if not self._source:
|
||||
self._source = source_res.group(1)
|
||||
self._last_token = self._source.upper()
|
||||
return
|
||||
effect_res = re.search(r"(%s)" % self._effect_re, token, re.IGNORECASE)
|
||||
if effect_res:
|
||||
self._last_token_type = "effect"
|
||||
|
||||
@@ -46,11 +46,13 @@ class Site(Base):
|
||||
# 流控间隔
|
||||
limit_seconds = Column(Integer, default=0)
|
||||
# 超时时间
|
||||
timeout = Column(Integer, default=0)
|
||||
timeout = Column(Integer, default=15)
|
||||
# 是否启用
|
||||
is_active = Column(Boolean(), default=True)
|
||||
# 创建时间
|
||||
lst_mod_date = Column(String, default=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
||||
# 下载器
|
||||
downloader = Column(String)
|
||||
|
||||
@staticmethod
|
||||
@db_query
|
||||
|
||||
@@ -64,6 +64,8 @@ class Subscribe(Base):
|
||||
username = Column(String)
|
||||
# 订阅站点
|
||||
sites = Column(JSON, default=list)
|
||||
# 下载器
|
||||
downloader = Column(String)
|
||||
# 是否洗版
|
||||
best_version = Column(Integer, default=0)
|
||||
# 当前优先级
|
||||
|
||||
@@ -287,7 +287,7 @@ class TorrentHelper(metaclass=Singleton):
|
||||
if not file:
|
||||
continue
|
||||
file_path = Path(file)
|
||||
if file_path.suffix not in settings.RMT_MEDIAEXT:
|
||||
if not file_path.suffix or file_path.suffix.lower() not in settings.RMT_MEDIAEXT:
|
||||
continue
|
||||
# 只使用文件名识别
|
||||
meta = MetaInfo(file_path.stem)
|
||||
|
||||
@@ -268,7 +268,7 @@ class FileManagerModule(_ModuleBase):
|
||||
return None
|
||||
return storage_oper.download(fileitem, path=path)
|
||||
|
||||
def upload_file(self, fileitem: FileItem, path: Path) -> Optional[FileItem]:
|
||||
def upload_file(self, fileitem: FileItem, path: Path, new_name: str = None) -> Optional[FileItem]:
|
||||
"""
|
||||
上传文件
|
||||
"""
|
||||
@@ -276,7 +276,7 @@ class FileManagerModule(_ModuleBase):
|
||||
if not storage_oper:
|
||||
logger.error(f"不支持 {fileitem.storage} 的上传处理")
|
||||
return None
|
||||
return storage_oper.upload(fileitem, path)
|
||||
return storage_oper.upload(fileitem, path, new_name)
|
||||
|
||||
def get_file_item(self, storage: str, path: Path) -> Optional[FileItem]:
|
||||
"""
|
||||
@@ -487,13 +487,8 @@ class FileManagerModule(_ModuleBase):
|
||||
target_fileitem = target_oper.get_folder(target_file.parent)
|
||||
if target_fileitem:
|
||||
# 上传文件
|
||||
new_item = target_oper.upload(target_fileitem, filepath)
|
||||
new_item = target_oper.upload(target_fileitem, filepath, target_file.name)
|
||||
if new_item:
|
||||
# 重命名为目标文件名
|
||||
if new_item.name != target_file.name:
|
||||
if target_oper.rename(new_item, target_file.name):
|
||||
new_item.name = target_file.name
|
||||
new_item.path = str(Path(new_item.path).parent / target_file.name)
|
||||
return new_item, ""
|
||||
else:
|
||||
return None, f"{fileitem.path} 上传 {target_storage} 失败"
|
||||
@@ -505,13 +500,8 @@ class FileManagerModule(_ModuleBase):
|
||||
target_fileitem = target_oper.get_folder(target_file.parent)
|
||||
if target_fileitem:
|
||||
# 上传文件
|
||||
new_item = target_oper.upload(target_fileitem, filepath)
|
||||
new_item = target_oper.upload(target_fileitem, filepath, target_file.name)
|
||||
if new_item:
|
||||
# 重命名为目标文件名
|
||||
if new_item.name != target_file.name:
|
||||
if target_oper.rename(new_item, target_file.name):
|
||||
new_item.name = target_file.name
|
||||
new_item.path = str(Path(new_item.path).parent / target_file.name)
|
||||
# 删除源文件
|
||||
source_oper.delete(fileitem)
|
||||
return new_item, ""
|
||||
|
||||
@@ -119,11 +119,12 @@ class StorageBase(metaclass=ABCMeta):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def upload(self, fileitem: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]:
|
||||
def upload(self, fileitem: schemas.FileItem, path: Path, new_name: str = None) -> Optional[schemas.FileItem]:
|
||||
"""
|
||||
上传文件
|
||||
:param fileitem: 上传目录项
|
||||
:param path: 本地文件路径
|
||||
:param new_name: 上传后文件名
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@@ -347,13 +347,13 @@ class AliPan(StorageBase):
|
||||
"""
|
||||
if not self.aligo:
|
||||
return None
|
||||
local_path = self.aligo.download_file(file_id=fileitem.fileid, drive_id=fileitem.drive_id,
|
||||
local_path = self.aligo.download_file(file_id=fileitem.fileid, drive_id=fileitem.drive_id, # noqa
|
||||
local_folder=str(path or settings.TEMP_PATH))
|
||||
if local_path:
|
||||
return Path(local_path)
|
||||
return None
|
||||
|
||||
def upload(self, fileitem: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]:
|
||||
def upload(self, fileitem: schemas.FileItem, path: Path, new_name: str = None) -> Optional[schemas.FileItem]:
|
||||
"""
|
||||
上传文件,并标记完成
|
||||
"""
|
||||
@@ -361,7 +361,7 @@ class AliPan(StorageBase):
|
||||
return None
|
||||
# 上传文件
|
||||
result = self.aligo.upload_file(file_path=str(path), parent_file_id=fileitem.fileid,
|
||||
drive_id=fileitem.drive_id, name=path.name,
|
||||
drive_id=fileitem.drive_id, name=new_name or path.name,
|
||||
check_name_mode="refuse")
|
||||
if result:
|
||||
item = self.aligo.get_file(file_id=result.file_id, drive_id=result.drive_id)
|
||||
|
||||
@@ -57,9 +57,6 @@ class Alist(StorageBase):
|
||||
如果设置永久令牌则返回永久令牌
|
||||
否则使用账号密码生成临时令牌
|
||||
"""
|
||||
token = self.get_conf().get("token")
|
||||
if token:
|
||||
return token
|
||||
return self.__generate_token
|
||||
|
||||
@property
|
||||
@@ -216,8 +213,8 @@ class Alist(StorageBase):
|
||||
path=(Path(fileitem.path) / item["name"]).as_posix() + ("/" if item["is_dir"] else ""),
|
||||
name=item["name"],
|
||||
basename=Path(item["name"]).stem,
|
||||
extension=Path(item["name"]).suffix,
|
||||
size=item["size"],
|
||||
extension=Path(item["name"]).suffix[1:] if not item["is_dir"] else None,
|
||||
size=item["size"] if not item["is_dir"] else None,
|
||||
modify_time=self.__parse_timestamp(item["modified"]),
|
||||
thumbnail=item["thumb"],
|
||||
)
|
||||
@@ -354,7 +351,7 @@ class Alist(StorageBase):
|
||||
path=path.as_posix() + ("/" if result["data"]["is_dir"] else ""),
|
||||
name=result["data"]["name"],
|
||||
basename=Path(result["data"]["name"]).stem,
|
||||
extension=Path(result["data"]["name"]).suffix,
|
||||
extension=Path(result["data"]["name"]).suffix[1:],
|
||||
size=result["data"]["size"],
|
||||
modify_time=self.__parse_timestamp(result["data"]["modified"]),
|
||||
thumbnail=result["data"]["thumb"],
|
||||
@@ -524,22 +521,25 @@ class Alist(StorageBase):
|
||||
).get_res(download_url)
|
||||
|
||||
if not path:
|
||||
path = settings.TEMP_PATH / fileitem.name
|
||||
new_path = settings.TEMP_PATH / fileitem.name
|
||||
else:
|
||||
new_path = path / fileitem.name
|
||||
|
||||
with open(path, "wb") as f:
|
||||
with open(new_path, "wb") as f:
|
||||
f.write(resp.content)
|
||||
|
||||
if path.exists():
|
||||
return path
|
||||
if new_path.exists():
|
||||
return new_path
|
||||
return None
|
||||
|
||||
def upload(
|
||||
self, fileitem: schemas.FileItem, path: Path, task: bool = False
|
||||
self, fileitem: schemas.FileItem, path: Path, new_name: str = None, task: bool = False
|
||||
) -> Optional[schemas.FileItem]:
|
||||
"""
|
||||
上传文件
|
||||
:param fileitem: 上传目录项
|
||||
:param path: 本地文件路径
|
||||
:param new_name: 上传后文件名
|
||||
:param task: 是否为任务,默认为False避免未完成上传时对文件进行操作
|
||||
"""
|
||||
encoded_path = UrlUtils.quote(fileitem.path)
|
||||
@@ -557,7 +557,12 @@ class Alist(StorageBase):
|
||||
logging.warning(f"请求上传文件 {path} 失败,状态码:{resp.status_code}")
|
||||
return
|
||||
|
||||
return fileitem
|
||||
new_item = self.get_item(Path(fileitem.path) / path.name)
|
||||
if new_name and new_name != path.name:
|
||||
if self.rename(new_item, new_name):
|
||||
return self.get_item(Path(new_item.path).with_name(new_name))
|
||||
|
||||
return new_item
|
||||
|
||||
def detail(self, fileitem: schemas.FileItem) -> Optional[schemas.FileItem]:
|
||||
"""
|
||||
|
||||
@@ -183,17 +183,17 @@ class LocalStorage(StorageBase):
|
||||
"""
|
||||
return Path(fileitem.path)
|
||||
|
||||
def upload(self, fileitem: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]:
|
||||
def upload(self, fileitem: schemas.FileItem, path: Path, new_name: str = None) -> Optional[schemas.FileItem]:
|
||||
"""
|
||||
上传文件
|
||||
"""
|
||||
dir_path = Path(fileitem.path)
|
||||
target_path = dir_path / path.name
|
||||
target_path = dir_path / (new_name or path.name)
|
||||
code, message = SystemUtils.move(path, target_path)
|
||||
if code != 0:
|
||||
logger.error(f"移动文件失败:{message}")
|
||||
return None
|
||||
return self.__get_diritem(target_path)
|
||||
return self.get_item(target_path)
|
||||
|
||||
def copy(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
|
||||
"""
|
||||
|
||||
@@ -39,7 +39,7 @@ class Rclone(StorageBase):
|
||||
path = Path(filepath)
|
||||
if not path.parent.exists():
|
||||
path.parent.mkdir(parents=True)
|
||||
path.write_text(conf.get('content'))
|
||||
path.write_text(conf.get('content'), encoding='utf-8')
|
||||
|
||||
@staticmethod
|
||||
def __get_hidden_shell():
|
||||
@@ -76,7 +76,7 @@ class Rclone(StorageBase):
|
||||
return schemas.FileItem(
|
||||
storage=self.schema.value,
|
||||
type="dir",
|
||||
path=f"{parent}{item.get('Name')}",
|
||||
path=f"{parent}{item.get('Name')}" + "/",
|
||||
name=item.get("Name"),
|
||||
basename=item.get("Name"),
|
||||
modify_time=StringUtils.str_to_timestamp(item.get("ModTime"))
|
||||
@@ -260,21 +260,22 @@ class Rclone(StorageBase):
|
||||
logger.error(f"rclone复制文件失败:{err}")
|
||||
return None
|
||||
|
||||
def upload(self, fileitem: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]:
|
||||
def upload(self, fileitem: schemas.FileItem, path: Path, new_name: str = None) -> Optional[schemas.FileItem]:
|
||||
"""
|
||||
上传文件
|
||||
"""
|
||||
try:
|
||||
new_path = Path(fileitem.path) / (new_name or path.name)
|
||||
retcode = subprocess.run(
|
||||
[
|
||||
'rclone', 'copyto',
|
||||
str(path),
|
||||
f'MP:{Path(fileitem.path) / path.name}'
|
||||
f'MP:{new_path}'
|
||||
],
|
||||
startupinfo=self.__get_hidden_shell()
|
||||
).returncode
|
||||
if retcode == 0:
|
||||
return self.__get_fileitem(path)
|
||||
return self.__get_fileitem(new_path)
|
||||
except Exception as err:
|
||||
logger.error(f"rclone上传文件失败:{err}")
|
||||
return None
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import base64
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple, List
|
||||
|
||||
import oss2
|
||||
import py115
|
||||
from py115 import Cloud
|
||||
from py115.types import LoginTarget, QrcodeSession, QrcodeStatus, Credential
|
||||
from p115 import P115Client, P115FileSystem, P115Path
|
||||
|
||||
from app import schemas
|
||||
from app.core.config import settings
|
||||
from app.log import logger
|
||||
from app.modules.filemanager.storages import StorageBase
|
||||
from app.schemas.types import StorageSchema
|
||||
from app.utils.http import RequestUtils
|
||||
from app.utils.singleton import Singleton
|
||||
|
||||
|
||||
@@ -27,36 +21,23 @@ class U115Pan(StorageBase, metaclass=Singleton):
|
||||
|
||||
# 支持的整理方式
|
||||
transtype = {
|
||||
"move": "移动"
|
||||
"move": "移动",
|
||||
"copy": "复制"
|
||||
}
|
||||
|
||||
cloud: Optional[Cloud] = None
|
||||
_session: QrcodeSession = None
|
||||
client: P115Client = None
|
||||
fs: P115FileSystem = None
|
||||
session_info: dict = None
|
||||
|
||||
# 是否有aria2c
|
||||
_has_aria2c: bool = False
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
try:
|
||||
subprocess.run(['aria2c', '-h'], capture_output=True)
|
||||
self._has_aria2c = True
|
||||
logger.debug('发现 aria2c, 将使用 aria2c 下载文件')
|
||||
except FileNotFoundError:
|
||||
logger.debug('未发现 aria2c')
|
||||
self._has_aria2c = False
|
||||
|
||||
def __init_cloud(self) -> bool:
|
||||
def __init_cloud(self, force: bool = False) -> bool:
|
||||
"""
|
||||
初始化Cloud
|
||||
"""
|
||||
credential = self.__credential
|
||||
if not credential:
|
||||
logger.warn("115未登录,请先登录!")
|
||||
return False
|
||||
try:
|
||||
if not self.cloud:
|
||||
self.cloud = py115.connect(credential)
|
||||
if not self.client or not self.client.cookies or force:
|
||||
self.client = P115Client(self.__credential,
|
||||
check_for_relogin=True, app="alipaymini", console_qrcode=False)
|
||||
self.fs = P115FileSystem(self.client)
|
||||
except Exception as err:
|
||||
logger.error(f"115连接失败,请重新扫码登录:{str(err)}")
|
||||
self.__clear_credential()
|
||||
@@ -64,20 +45,23 @@ class U115Pan(StorageBase, metaclass=Singleton):
|
||||
return True
|
||||
|
||||
@property
|
||||
def __credential(self) -> Optional[Credential]:
|
||||
def __credential(self) -> Optional[str]:
|
||||
"""
|
||||
获取已保存的115认证参数
|
||||
获取已保存的115 Cookie
|
||||
"""
|
||||
cookie_dict = self.get_config()
|
||||
if not cookie_dict:
|
||||
conf = self.get_config()
|
||||
if not conf:
|
||||
return None
|
||||
return Credential.from_dict(cookie_dict.dict().get("config"))
|
||||
if not conf.config:
|
||||
return None
|
||||
# 将dict转换为cookie字符串格式
|
||||
return "; ".join([f"{k}={v}" for k, v in conf.config.items()])
|
||||
|
||||
def __save_credential(self, credential: Credential):
|
||||
def __save_credential(self, credential: dict):
|
||||
"""
|
||||
设置115认证参数
|
||||
"""
|
||||
self.set_config(credential.to_dict())
|
||||
self.set_config(credential)
|
||||
|
||||
def __clear_credential(self):
|
||||
"""
|
||||
@@ -89,17 +73,17 @@ class U115Pan(StorageBase, metaclass=Singleton):
|
||||
"""
|
||||
生成二维码
|
||||
"""
|
||||
if not self.__init_cloud():
|
||||
return None
|
||||
try:
|
||||
self.cloud = py115.connect()
|
||||
self._session = self.cloud.qrcode_login(LoginTarget.Web)
|
||||
image_bin = self._session.image_data
|
||||
if not image_bin:
|
||||
resp = self.client.login_qrcode_token()
|
||||
self.session_info = resp["data"]
|
||||
qrcode_content = self.session_info.pop("qrcode")
|
||||
if not qrcode_content:
|
||||
logger.warn("115生成二维码失败:未获取到二维码数据!")
|
||||
return None
|
||||
# 转换为base64图片格式
|
||||
image_base64 = base64.b64encode(image_bin).decode()
|
||||
return {
|
||||
"codeContent": f"data:image/jpeg;base64,{image_base64}"
|
||||
"codeContent": qrcode_content
|
||||
}, ""
|
||||
except Exception as e:
|
||||
logger.warn(f"115生成二维码失败:{str(e)}")
|
||||
@@ -109,42 +93,48 @@ class U115Pan(StorageBase, metaclass=Singleton):
|
||||
"""
|
||||
二维码登录确认
|
||||
"""
|
||||
if not self._session:
|
||||
if not self.session_info:
|
||||
return {}, "请先生成二维码!"
|
||||
try:
|
||||
if not self.cloud:
|
||||
return {}, "请先生成二维码!"
|
||||
status = self.cloud.qrcode_poll(self._session)
|
||||
if status == QrcodeStatus.Done:
|
||||
# 确认完成,保存认证信息
|
||||
self.__save_credential(self.cloud.export_credentail())
|
||||
result = {
|
||||
"status": 1,
|
||||
"tip": "登录成功!"
|
||||
}
|
||||
elif status == QrcodeStatus.Waiting:
|
||||
result = {
|
||||
"status": 0,
|
||||
"tip": "请使用微信或115客户端扫码"
|
||||
}
|
||||
elif status == QrcodeStatus.Expired:
|
||||
result = {
|
||||
"status": -1,
|
||||
"tip": "二维码已过期,请重新刷新!"
|
||||
}
|
||||
self.cloud = None
|
||||
elif status == QrcodeStatus.Failed:
|
||||
result = {
|
||||
"status": -2,
|
||||
"tip": "登录失败,请重试!"
|
||||
}
|
||||
self.cloud = None
|
||||
else:
|
||||
result = {
|
||||
"status": -3,
|
||||
"tip": "未知错误,请重试!"
|
||||
}
|
||||
self.cloud = None
|
||||
resp = self.client.login_qrcode_scan_status(self.session_info)
|
||||
match resp["data"].get("status"):
|
||||
case 0:
|
||||
result = {
|
||||
"status": 0,
|
||||
"tip": "请使用微信或115客户端扫码"
|
||||
}
|
||||
case 1:
|
||||
result = {
|
||||
"status": 1,
|
||||
"tip": "已扫码"
|
||||
}
|
||||
case 2:
|
||||
# 确认完成,保存认证信息
|
||||
resp = self.client.login_qrcode_scan_result(uid=self.session_info.get("uid"),
|
||||
app="alipaymini")
|
||||
if resp:
|
||||
# 保存认证信息
|
||||
self.__save_credential(resp["data"]["cookie"])
|
||||
self.__init_cloud(force=True)
|
||||
result = {
|
||||
"status": 2,
|
||||
"tip": "登录成功!"
|
||||
}
|
||||
case -1:
|
||||
result = {
|
||||
"status": -1,
|
||||
"tip": "二维码已过期,请重新刷新!"
|
||||
}
|
||||
case -2:
|
||||
result = {
|
||||
"status": -2,
|
||||
"tip": "登录失败,请重试!"
|
||||
}
|
||||
case _:
|
||||
result = {
|
||||
"status": -3,
|
||||
"tip": "未知错误,请重试!"
|
||||
}
|
||||
return result, ""
|
||||
except Exception as e:
|
||||
return {}, f"115登录确认失败:{str(e)}"
|
||||
@@ -156,7 +146,9 @@ class U115Pan(StorageBase, metaclass=Singleton):
|
||||
if not self.__init_cloud():
|
||||
return None
|
||||
try:
|
||||
return self.cloud.storage().space()
|
||||
usage = self.fs.space_summury()
|
||||
if usage:
|
||||
return usage['rt_space_info']['all_total']['size'], usage['rt_space_info']['all_remain']['size']
|
||||
except Exception as e:
|
||||
logger.error(f"115获取存储空间失败:{str(e)}")
|
||||
return None
|
||||
@@ -165,9 +157,7 @@ class U115Pan(StorageBase, metaclass=Singleton):
|
||||
"""
|
||||
检查存储是否可用
|
||||
"""
|
||||
return True if self.list(schemas.FileItem(
|
||||
fileid="0"
|
||||
)) else False
|
||||
return True if self.list(schemas.FileItem()) else False
|
||||
|
||||
def list(self, fileitem: schemas.FileItem) -> Optional[List[schemas.FileItem]]:
|
||||
"""
|
||||
@@ -178,18 +168,16 @@ class U115Pan(StorageBase, metaclass=Singleton):
|
||||
try:
|
||||
if fileitem.type == "file":
|
||||
return [fileitem]
|
||||
items = self.cloud.storage().list(dir_id=fileitem.fileid)
|
||||
items: List[P115Path] = self.fs.list(fileitem.path)
|
||||
return [schemas.FileItem(
|
||||
storage=self.schema.value,
|
||||
fileid=item.file_id,
|
||||
parent_fileid=item.parent_id,
|
||||
type="dir" if item.is_dir else "file",
|
||||
path=f"{fileitem.path}{item.name}" + ("/" if item.is_dir else ""),
|
||||
type="dir" if item.is_dir() else "file",
|
||||
path=item.path + ("/" if item.is_dir() else ""),
|
||||
name=item.name,
|
||||
size=item.size,
|
||||
extension=Path(item.name).suffix[1:],
|
||||
modify_time=item.modified_time.timestamp() if item.modified_time else 0,
|
||||
pickcode=item.pickcode
|
||||
basename=item.stem,
|
||||
size=item.stat().st_size,
|
||||
extension=item.suffix[1:] if not item.is_dir() else None,
|
||||
modify_time=item.stat().st_mtime
|
||||
) for item in items if item]
|
||||
except Exception as e:
|
||||
logger.error(f"115浏览文件失败:{str(e)}")
|
||||
@@ -202,17 +190,15 @@ class U115Pan(StorageBase, metaclass=Singleton):
|
||||
if not self.__init_cloud():
|
||||
return None
|
||||
try:
|
||||
result = self.cloud.storage().make_dir(fileitem.fileid, name)
|
||||
result = self.fs.makedirs(Path(fileitem.path) / name, exist_ok=True)
|
||||
if result:
|
||||
return schemas.FileItem(
|
||||
storage=self.schema.value,
|
||||
fileid=result.file_id,
|
||||
parent_fileid=result.parent_id,
|
||||
type="dir",
|
||||
path=f"{fileitem.path}{name}/",
|
||||
path=f"{result.path}/",
|
||||
name=name,
|
||||
modify_time=result.modified_time.timestamp() if result.modified_time else 0,
|
||||
pickcode=result.pickcode
|
||||
basename=Path(result.name).stem,
|
||||
modify_time=result.mtime
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"115创建目录失败:{str(e)}")
|
||||
@@ -222,64 +208,74 @@ class U115Pan(StorageBase, metaclass=Singleton):
|
||||
"""
|
||||
根据文件路程获取目录,不存在则创建
|
||||
"""
|
||||
|
||||
def __find_dir(_fileitem: schemas.FileItem, _name: str) -> Optional[schemas.FileItem]:
|
||||
"""
|
||||
查找下级目录中匹配名称的目录
|
||||
"""
|
||||
for sub_file in self.list(_fileitem):
|
||||
if sub_file.type != "dir":
|
||||
continue
|
||||
if sub_file.name == _name:
|
||||
return sub_file
|
||||
if not self.__init_cloud():
|
||||
return None
|
||||
|
||||
# 逐级查找和创建目录
|
||||
fileitem = schemas.FileItem(fileid="0")
|
||||
for part in path.parts:
|
||||
if part == "/":
|
||||
continue
|
||||
dir_file = __find_dir(fileitem, part)
|
||||
if dir_file:
|
||||
fileitem = dir_file
|
||||
else:
|
||||
dir_file = self.create_folder(fileitem, part)
|
||||
if not dir_file:
|
||||
logger.warn(f"115创建目录 {fileitem.path}{part} 失败!")
|
||||
return None
|
||||
fileitem = dir_file
|
||||
return fileitem if fileitem.fileid != "0" else None
|
||||
try:
|
||||
result = self.fs.makedirs(path, exist_ok=True)
|
||||
if result:
|
||||
return schemas.FileItem(
|
||||
storage=self.schema.value,
|
||||
type="dir",
|
||||
path=result.path + "/",
|
||||
name=result.name,
|
||||
basename=Path(result.name).stem,
|
||||
modify_time=result.mtime
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"115获取目录失败:{str(e)}")
|
||||
return None
|
||||
|
||||
def get_item(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
"""
|
||||
获取文件或目录,不存在返回None
|
||||
"""
|
||||
|
||||
def __find_item(_fileitem: schemas.FileItem, _name: str) -> Optional[schemas.FileItem]:
|
||||
"""
|
||||
查找下级目录中匹配名称的目录或文件
|
||||
"""
|
||||
for sub_file in self.list(_fileitem):
|
||||
if sub_file.name == _name:
|
||||
return sub_file
|
||||
if not self.__init_cloud():
|
||||
return None
|
||||
|
||||
# 逐级查找
|
||||
fileitem = schemas.FileItem(fileid="0")
|
||||
for part in path.parts:
|
||||
if part == "/":
|
||||
continue
|
||||
item = __find_item(fileitem, part)
|
||||
if not item:
|
||||
try:
|
||||
try:
|
||||
item = self.fs.attr(path)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
fileitem = item
|
||||
return fileitem
|
||||
if item:
|
||||
return schemas.FileItem(
|
||||
storage=self.schema.value,
|
||||
type="dir" if item.is_directory else "file",
|
||||
path=item.path + ("/" if item.is_directory else ""),
|
||||
name=item.name,
|
||||
size=item.size,
|
||||
extension=Path(item.name).suffix[1:] if not item.is_directory else None,
|
||||
modify_time=item.mtime,
|
||||
thumbnail=item.get("thumb")
|
||||
)
|
||||
except Exception as e:
|
||||
logger.info(f"115获取文件失败:{str(e)}")
|
||||
return None
|
||||
|
||||
def detail(self, fileitem: schemas.FileItem) -> Optional[schemas.FileItem]:
|
||||
"""
|
||||
获取文件详情
|
||||
"""
|
||||
pass
|
||||
if not self.__init_cloud():
|
||||
return None
|
||||
try:
|
||||
try:
|
||||
item = self.fs.attr(fileitem.path)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
if item:
|
||||
return schemas.FileItem(
|
||||
storage=self.schema.value,
|
||||
type="dir" if item.is_directory else "file",
|
||||
path=item.path + ("/" if item.is_directory else ""),
|
||||
name=item.name,
|
||||
size=item.size,
|
||||
extension=Path(item.name).suffix[1:] if not item.is_directory else None,
|
||||
modify_time=item.mtime,
|
||||
thumbnail=item.get("thumb")
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"115获取文件详情失败:{str(e)}")
|
||||
return None
|
||||
|
||||
def delete(self, fileitem: schemas.FileItem) -> bool:
|
||||
"""
|
||||
@@ -288,7 +284,7 @@ class U115Pan(StorageBase, metaclass=Singleton):
|
||||
if not self.__init_cloud():
|
||||
return False
|
||||
try:
|
||||
self.cloud.storage().delete(fileitem.fileid)
|
||||
self.fs.remove(fileitem.path)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"115删除文件失败:{str(e)}")
|
||||
@@ -301,7 +297,7 @@ class U115Pan(StorageBase, metaclass=Singleton):
|
||||
if not self.__init_cloud():
|
||||
return False
|
||||
try:
|
||||
self.cloud.storage().rename(fileitem.fileid, name)
|
||||
self.fs.rename(fileitem.path, Path(fileitem.path).with_name(name))
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"115重命名文件失败:{str(e)}")
|
||||
@@ -313,67 +309,36 @@ class U115Pan(StorageBase, metaclass=Singleton):
|
||||
"""
|
||||
if not self.__init_cloud():
|
||||
return None
|
||||
local_file = (path or settings.TEMP_PATH) / fileitem.name
|
||||
try:
|
||||
ticket = self.cloud.storage().request_download(fileitem.pickcode)
|
||||
if ticket:
|
||||
path = (path or settings.TEMP_PATH) / fileitem.name
|
||||
res = RequestUtils(headers=ticket.headers).get_res(ticket.url)
|
||||
if res:
|
||||
with open(path, "wb") as f:
|
||||
f.write(res.content)
|
||||
return path
|
||||
else:
|
||||
logger.warn(f"{fileitem.path} 未获取到下载链接")
|
||||
task = self.fs.download(fileitem.path, file=local_file)
|
||||
if task:
|
||||
return local_file
|
||||
except Exception as e:
|
||||
logger.error(f"115下载失败:{str(e)}")
|
||||
logger.error(f"115下载文件失败:{str(e)}")
|
||||
return None
|
||||
|
||||
def upload(self, fileitem: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]:
|
||||
def upload(self, fileitem: schemas.FileItem, path: Path, new_name: str = None) -> Optional[schemas.FileItem]:
|
||||
"""
|
||||
上传文件
|
||||
"""
|
||||
if not self.__init_cloud():
|
||||
return None
|
||||
try:
|
||||
ticket = self.cloud.storage().request_upload(dir_id=fileitem.fileid, file_path=str(path))
|
||||
if ticket is None:
|
||||
logger.warn(f"115请求上传出错")
|
||||
return None
|
||||
elif ticket.is_done:
|
||||
file_path = Path(fileitem.path) / path.name
|
||||
logger.warn(f"115上传:{file_path} 文件已存在")
|
||||
return self.get_item(file_path)
|
||||
else:
|
||||
auth = oss2.StsAuth(**ticket.oss_token)
|
||||
bucket = oss2.Bucket(
|
||||
auth=auth,
|
||||
endpoint=ticket.oss_endpoint,
|
||||
bucket_name=ticket.bucket_name,
|
||||
)
|
||||
por = bucket.put_object_from_file(
|
||||
key=ticket.object_key,
|
||||
filename=str(path),
|
||||
headers=ticket.headers,
|
||||
)
|
||||
result = por.resp.response.json()
|
||||
new_path = Path(fileitem.path) / (new_name or path.name)
|
||||
with open(path, "rb") as f:
|
||||
result = self.fs.upload(f, new_path)
|
||||
if result:
|
||||
result_data = result.get('data')
|
||||
logger.info(f"115上传文件成功:{result_data.get('file_name')}")
|
||||
return schemas.FileItem(
|
||||
storage=self.schema.value,
|
||||
fileid=result_data.get('file_id'),
|
||||
parent_fileid=fileitem.fileid,
|
||||
type="file",
|
||||
name=result_data.get('file_name'),
|
||||
basename=Path(result_data.get('file_name')).stem,
|
||||
path=f"{fileitem.path}{result_data.get('file_name')}",
|
||||
size=result_data.get('file_size'),
|
||||
extension=Path(result_data.get('file_name')).suffix[1:],
|
||||
pickcode=result_data.get('pickcode')
|
||||
path=str(path),
|
||||
name=result.name,
|
||||
basename=Path(result.name).stem,
|
||||
size=result.size,
|
||||
extension=Path(result.name).suffix[1:],
|
||||
modify_time=result.mtime
|
||||
)
|
||||
else:
|
||||
logger.warn(f"115上传文件失败:{por.resp.response.text}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"115上传文件失败:{str(e)}")
|
||||
return None
|
||||
@@ -385,14 +350,24 @@ class U115Pan(StorageBase, metaclass=Singleton):
|
||||
if not self.__init_cloud():
|
||||
return False
|
||||
try:
|
||||
self.cloud.storage().move(fileitem.fileid, target.fileid)
|
||||
self.fs.move(fileitem.path, target.path)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"115移动文件失败:{str(e)}")
|
||||
return False
|
||||
|
||||
def copy(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
|
||||
pass
|
||||
"""
|
||||
复制文件
|
||||
"""
|
||||
if not self.__init_cloud():
|
||||
return False
|
||||
try:
|
||||
self.fs.copy(fileitem.path, target_file)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"115复制文件失败:{str(e)}")
|
||||
return False
|
||||
|
||||
def link(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
|
||||
pass
|
||||
@@ -406,9 +381,9 @@ class U115Pan(StorageBase, metaclass=Singleton):
|
||||
"""
|
||||
info = self.storage()
|
||||
if info:
|
||||
total, used = info
|
||||
total, free = info
|
||||
return schemas.StorageUsage(
|
||||
total=total,
|
||||
available=total - used
|
||||
available=free
|
||||
)
|
||||
return schemas.StorageUsage()
|
||||
|
||||
@@ -191,6 +191,7 @@ class IndexerModule(_ModuleBase):
|
||||
site_ua=site.get("ua"),
|
||||
site_proxy=site.get("proxy"),
|
||||
site_order=site.get("pri"),
|
||||
site_downloader=site.get("downloader"),
|
||||
**result) for result in result_array]
|
||||
# 去重
|
||||
return __remove_duplicate(torrents)
|
||||
@@ -199,7 +200,7 @@ class IndexerModule(_ModuleBase):
|
||||
def __spider_search(indexer: CommentedMap,
|
||||
search_word: str = None,
|
||||
mtype: MediaType = None,
|
||||
page: int = 0) -> (bool, List[dict]):
|
||||
page: int = 0) -> Tuple[bool, List[dict]]:
|
||||
"""
|
||||
根据关键字搜索单个站点
|
||||
:param: indexer: 站点配置
|
||||
|
||||
@@ -94,6 +94,7 @@ class SiteParserBase(metaclass=ABCMeta):
|
||||
# 未读消息
|
||||
self.message_unread = 0
|
||||
self.message_unread_contents = []
|
||||
self.message_read_force = False
|
||||
|
||||
# 全局附加请求头
|
||||
self._addition_headers = None
|
||||
@@ -202,7 +203,7 @@ class SiteParserBase(metaclass=ABCMeta):
|
||||
:return:
|
||||
"""
|
||||
unread_msg_links = []
|
||||
if self.message_unread > 0:
|
||||
if self.message_unread > 0 or self.message_read_force:
|
||||
links = {self._user_mail_unread_page, self._sys_mail_unread_page}
|
||||
for link in links:
|
||||
if not link:
|
||||
@@ -226,7 +227,7 @@ class SiteParserBase(metaclass=ABCMeta):
|
||||
)
|
||||
unread_msg_links.extend(msg_links)
|
||||
# 重新更新未读消息数(99999表示有消息但数量未知)
|
||||
if self.message_unread == 99999:
|
||||
if unread_msg_links and not self.message_unread:
|
||||
self.message_unread = len(unread_msg_links)
|
||||
# 解析未读消息内容
|
||||
for msg_link in unread_msg_links:
|
||||
|
||||
@@ -91,9 +91,7 @@ class MTorrentSiteUserInfo(SiteParserBase):
|
||||
self.download = int(user_info.get("memberCount", {}).get("downloaded") or '0')
|
||||
self.ratio = user_info.get("memberCount", {}).get("shareRate") or 0
|
||||
self.bonus = user_info.get("memberCount", {}).get("bonus") or 0
|
||||
# 需要解析消息,但不确定消息条数
|
||||
self.message_unread = 99999
|
||||
|
||||
self.message_read_force = True
|
||||
self._torrent_seeding_params = {
|
||||
"pageNumber": 1,
|
||||
"pageSize": 200,
|
||||
|
||||
@@ -162,26 +162,26 @@ class Plex:
|
||||
def get_medias_count(self) -> schemas.Statistic:
|
||||
"""
|
||||
获得电影、电视剧、动漫媒体数量
|
||||
:return: MovieCount SeriesCount SongCount
|
||||
:return: movie_count tv_count episode_count
|
||||
"""
|
||||
if not self._plex:
|
||||
return schemas.Statistic()
|
||||
sections = self._plex.library.sections()
|
||||
MovieCount = SeriesCount = EpisodeCount = 0
|
||||
movie_count = tv_count = episode_count = 0
|
||||
# 媒体库白名单
|
||||
allow_library = [lib.id for lib in self.get_librarys(hidden=True)]
|
||||
for sec in sections:
|
||||
if str(sec.key) not in allow_library:
|
||||
if sec.key not in allow_library:
|
||||
continue
|
||||
if sec.type == "movie":
|
||||
MovieCount += sec.totalSize
|
||||
movie_count += sec.totalSize
|
||||
if sec.type == "show":
|
||||
SeriesCount += sec.totalSize
|
||||
EpisodeCount += sec.totalViewSize(libtype='episode')
|
||||
tv_count += sec.totalSize
|
||||
episode_count += sec.totalViewSize(libtype="episode")
|
||||
return schemas.Statistic(
|
||||
movie_count=MovieCount,
|
||||
tv_count=SeriesCount,
|
||||
episode_count=EpisodeCount
|
||||
movie_count=movie_count,
|
||||
tv_count=tv_count,
|
||||
episode_count=episode_count
|
||||
)
|
||||
|
||||
def get_movies(self,
|
||||
@@ -721,7 +721,7 @@ class Plex:
|
||||
if not self._plex:
|
||||
return []
|
||||
# 媒体库白名单
|
||||
allow_library = ",".join([lib.id for lib in self.get_librarys(hidden=True)])
|
||||
allow_library = ",".join(map(str, (lib.id for lib in self.get_librarys(hidden=True))))
|
||||
params = {"contentDirectoryID": allow_library}
|
||||
items = self._plex.fetchItems("/hubs/continueWatching/items",
|
||||
container_start=0,
|
||||
@@ -757,7 +757,7 @@ class Plex:
|
||||
if not self._plex:
|
||||
return None
|
||||
# 请求参数(除黑名单)
|
||||
allow_library = ",".join([lib.id for lib in self.get_librarys(hidden=True)])
|
||||
allow_library = ",".join(map(str, (lib.id for lib in self.get_librarys(hidden=True))))
|
||||
params = {
|
||||
"contentDirectoryID": allow_library,
|
||||
"count": num,
|
||||
|
||||
@@ -77,7 +77,7 @@ class Qbittorrent:
|
||||
try:
|
||||
qbt.auth_log_in()
|
||||
except (qbittorrentapi.LoginFailed, qbittorrentapi.Forbidden403Error) as e:
|
||||
logger.error(f"qbittorrent 登录失败:{str(e)}")
|
||||
logger.error(f"qbittorrent 登录失败:{str(e).strip() or '请检查用户名和密码是否正确'}")
|
||||
return None
|
||||
except Exception as e:
|
||||
stack_trace = "".join(traceback.format_exception(None, e, e.__traceback__))[:2000]
|
||||
|
||||
@@ -478,13 +478,7 @@ class Monitor(metaclass=Singleton):
|
||||
|
||||
# 移动模式删除空目录
|
||||
if transferinfo.transfer_type in ["move"]:
|
||||
if file_item.type == "dir":
|
||||
folder_item = file_item
|
||||
else:
|
||||
folder_item = self.storagechain.get_parent_item(file_item)
|
||||
if folder_item and not self.storagechain.any_files(folder_item, extensions=settings.RMT_MEDIAEXT):
|
||||
logger.warn(f"删除残留空文件夹:【{folder_item.storage}】{folder_item.path}")
|
||||
self.storagechain.delete_file(folder_item)
|
||||
self.storagechain.delete_media_file(file_item, delete_self=False)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("目录监控发生错误:%s - %s" % (str(e), traceback.format_exc()))
|
||||
|
||||
@@ -19,10 +19,11 @@ from app.chain.transfer import TransferChain
|
||||
from app.core.config import settings
|
||||
from app.core.event import EventManager
|
||||
from app.core.plugin import PluginManager
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.helper.sites import SitesHelper
|
||||
from app.log import logger
|
||||
from app.schemas import Notification, NotificationType
|
||||
from app.schemas.types import EventType
|
||||
from app.schemas.types import EventType, SystemConfigKey
|
||||
from app.utils.singleton import Singleton
|
||||
from app.utils.timer import TimerUtils
|
||||
|
||||
@@ -74,8 +75,12 @@ class Scheduler(metaclass=Singleton):
|
||||
message="用户认证失败次数过多,将不再尝试认证!",
|
||||
role="system")
|
||||
return
|
||||
logger.info("用户未认证,正在尝试重新认证...")
|
||||
status, msg = SitesHelper().check_user()
|
||||
logger.info("用户未认证,正在尝试认证...")
|
||||
auth_conf = SystemConfigOper().get(SystemConfigKey.UserSiteAuthParams)
|
||||
if auth_conf:
|
||||
status, msg = SitesHelper().check_user(**auth_conf)
|
||||
else:
|
||||
status, msg = SitesHelper().check_user()
|
||||
if status:
|
||||
self._auth_count = 0
|
||||
logger.info(f"{msg} 用户认证成功")
|
||||
@@ -169,6 +174,9 @@ class Scheduler(metaclass=Singleton):
|
||||
# 停止定时服务
|
||||
self.stop()
|
||||
|
||||
# 用户认证立即执行一次
|
||||
user_auth()
|
||||
|
||||
# 调试模式不启动定时服务
|
||||
if settings.DEV:
|
||||
return
|
||||
|
||||
@@ -180,6 +180,8 @@ class TorrentInfo(BaseModel):
|
||||
site_proxy: Optional[bool] = False
|
||||
# 站点优先级
|
||||
site_order: Optional[int] = 0
|
||||
# 站点下载器
|
||||
site_downloader: Optional[str] = None
|
||||
# 种子名称
|
||||
title: Optional[str] = None
|
||||
# 种子副标题
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Optional, Any, Union
|
||||
from typing import Optional, Any, Union, Dict
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -35,7 +35,7 @@ class Site(BaseModel):
|
||||
# 备注
|
||||
note: Optional[Any] = None
|
||||
# 超时时间
|
||||
timeout: Optional[int] = 0
|
||||
timeout: Optional[int] = 15
|
||||
# 流控单位周期
|
||||
limit_interval: Optional[int] = None
|
||||
# 流控次数
|
||||
@@ -44,6 +44,8 @@ class Site(BaseModel):
|
||||
limit_seconds: Optional[int] = None
|
||||
# 是否启用
|
||||
is_active: Optional[bool] = True
|
||||
# 下载器
|
||||
downloader: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
@@ -108,3 +110,8 @@ class SiteUserData(BaseModel):
|
||||
updated_day: Optional[str] = None
|
||||
# 更新时间
|
||||
updated_time: Optional[str] = None
|
||||
|
||||
|
||||
class SiteAuth(BaseModel):
|
||||
site: Optional[str] = None
|
||||
params: Optional[Dict[str, Union[int, str]]] = {}
|
||||
|
||||
@@ -54,6 +54,8 @@ class Subscribe(BaseModel):
|
||||
username: Optional[str] = None
|
||||
# 订阅站点
|
||||
sites: Optional[List[int]] = []
|
||||
# 下载器
|
||||
downloader: Optional[str] = None
|
||||
# 是否洗版
|
||||
best_version: Optional[int] = 0
|
||||
# 当前优先级
|
||||
|
||||
@@ -122,6 +122,8 @@ class SystemConfigKey(Enum):
|
||||
DefaultMovieSubscribeConfig = "DefaultMovieSubscribeConfig"
|
||||
# 默认电视剧订阅规则
|
||||
DefaultTvSubscribeConfig = "DefaultTvSubscribeConfig"
|
||||
# 用户站点认证参数
|
||||
UserSiteAuthParams = "UserSiteAuthParams"
|
||||
|
||||
|
||||
# 处理进度Key字典
|
||||
|
||||
@@ -275,6 +275,10 @@ class SystemUtils:
|
||||
# 遍历目录
|
||||
for path in directory.iterdir():
|
||||
if path.is_dir():
|
||||
if not SystemUtils.is_windows() and path.name.startswith("."):
|
||||
continue
|
||||
if path.name == "@eaDir":
|
||||
continue
|
||||
dirs.append(path)
|
||||
|
||||
return dirs
|
||||
|
||||
31
database/versions/eaf9cbc49027_2_0_7.py
Normal file
31
database/versions/eaf9cbc49027_2_0_7.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""2.0.7
|
||||
|
||||
Revision ID: eaf9cbc49027
|
||||
Revises: a295e41830a6
|
||||
Create Date: 2024-11-16 00:26:09.505188
|
||||
|
||||
"""
|
||||
import contextlib
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'eaf9cbc49027'
|
||||
down_revision = 'a295e41830a6'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
# 站点管理、订阅增加下载器选项
|
||||
with contextlib.suppress(Exception):
|
||||
op.add_column('site', sa.Column('downloader', sa.String(), nullable=True))
|
||||
op.add_column('subscribe', sa.Column('downloader', sa.String(), nullable=True))
|
||||
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@@ -3,7 +3,8 @@
|
||||
# shellcheck disable=SC2016
|
||||
|
||||
# 使用 `envsubst` 将模板文件中的 ${NGINX_PORT} 替换为实际的环境变量值
|
||||
envsubst '${NGINX_PORT}${PORT}' < /etc/nginx/nginx.template.conf > /etc/nginx/nginx.conf
|
||||
export NGINX_CLIENT_MAX_BODY_SIZE=${NGINX_CLIENT_MAX_BODY_SIZE:-10m}
|
||||
envsubst '${NGINX_PORT}${PORT}${NGINX_CLIENT_MAX_BODY_SIZE}' < /etc/nginx/nginx.template.conf > /etc/nginx/nginx.conf
|
||||
# 自动更新
|
||||
cd /
|
||||
/usr/local/bin/mp_update
|
||||
|
||||
@@ -17,6 +17,8 @@ http {
|
||||
|
||||
keepalive_timeout 3600;
|
||||
|
||||
client_max_body_size ${NGINX_CLIENT_MAX_BODY_SIZE};
|
||||
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
gzip_proxied any;
|
||||
|
||||
@@ -23,7 +23,7 @@ APScheduler~=3.10.1
|
||||
cryptography~=43.0.0
|
||||
pytz~=2023.3
|
||||
pycryptodome~=3.20.0
|
||||
qbittorrent-api==2024.9.67
|
||||
qbittorrent-api==2024.11.69
|
||||
plexapi~=4.15.16
|
||||
transmission-rpc~=4.3.0
|
||||
Jinja2~=3.1.4
|
||||
@@ -58,7 +58,6 @@ pystray~=0.19.5
|
||||
pyotp~=2.9.0
|
||||
Pinyin2Hanzi~=0.1.1
|
||||
pywebpush~=2.0.0
|
||||
py115j~=0.0.7
|
||||
oss2~=2.18.6
|
||||
python-115~=0.0.9.8.7
|
||||
aligo~=6.2.4
|
||||
aiofiles~=24.1.0
|
||||
@@ -345,13 +345,13 @@ meta_cases = [{
|
||||
"part": "",
|
||||
"season": "",
|
||||
"episode": "",
|
||||
"restype": "BluRay Remux",
|
||||
"restype": "BluRay REMUX",
|
||||
"pix": "1080p",
|
||||
"video_codec": "AVC",
|
||||
"audio_codec": "LPCM 7³"
|
||||
}
|
||||
}, {
|
||||
"title": "30.Rock.S02E01.1080p.BluRay.X264-BORDURE.mkv",
|
||||
"title": "30.Rock.S02E01.1080p.UHD.BluRay.X264-BORDURE.mkv",
|
||||
"subtitle": "",
|
||||
"target": {
|
||||
"type": "电视剧",
|
||||
@@ -361,7 +361,7 @@ meta_cases = [{
|
||||
"part": "",
|
||||
"season": "S02",
|
||||
"episode": "E01",
|
||||
"restype": "BluRay",
|
||||
"restype": "UHD BluRay",
|
||||
"pix": "1080p",
|
||||
"video_codec": "X264",
|
||||
"audio_codec": ""
|
||||
@@ -611,7 +611,7 @@ meta_cases = [{
|
||||
"subtitle": "",
|
||||
"target": {
|
||||
"type": "电视剧",
|
||||
"cn_name": "处刑少女的生存之道",
|
||||
"cn_name": "處刑少女的生存之道",
|
||||
"en_name": "",
|
||||
"year": "",
|
||||
"part": "",
|
||||
@@ -665,7 +665,7 @@ meta_cases = [{
|
||||
"part": "",
|
||||
"season": "",
|
||||
"episode": "",
|
||||
"restype": "BluRay DoVi UHD",
|
||||
"restype": "UHD BluRay DoVi",
|
||||
"pix": "1080p",
|
||||
"video_codec": "X265",
|
||||
"audio_codec": "DD 7.1"
|
||||
|
||||
59
update
59
update
@@ -20,6 +20,16 @@ function WARN() {
|
||||
echo -e "${WARN} ${1}"
|
||||
}
|
||||
|
||||
TMP_PATH=$(mktemp -d)
|
||||
if [ ! -d "${TMP_PATH}" ]; then
|
||||
# 如果自动生成 tmp 文件夹失败则手动指定,避免出现数据丢失等情况
|
||||
TMP_PATH=/tmp/mp_update_path
|
||||
if [ -d /tmp/mp_update_path ]; then
|
||||
rm -rf /tmp/mp_update_path
|
||||
fi
|
||||
mkdir -p /tmp/mp_update_path
|
||||
fi
|
||||
|
||||
# 下载及解压
|
||||
function download_and_unzip() {
|
||||
local retries=0
|
||||
@@ -28,9 +38,9 @@ function download_and_unzip() {
|
||||
local target_dir="$2"
|
||||
INFO "正在下载 ${url}..."
|
||||
while [ $retries -lt $max_retries ]; do
|
||||
if curl ${CURL_OPTIONS} "${url}" ${CURL_HEADERS} | busybox unzip -d /tmp - > /dev/null; then
|
||||
if [ -e /tmp/MoviePilot-* ]; then
|
||||
mv /tmp/MoviePilot-* /tmp/"${target_dir}"
|
||||
if curl ${CURL_OPTIONS} "${url}" ${CURL_HEADERS} | busybox unzip -d ${TMP_PATH} - > /dev/null; then
|
||||
if [ -e ${TMP_PATH}/MoviePilot-* ]; then
|
||||
mv ${TMP_PATH}/MoviePilot-* ${TMP_PATH}/"${target_dir}"
|
||||
fi
|
||||
break
|
||||
else
|
||||
@@ -48,8 +58,6 @@ function download_and_unzip() {
|
||||
|
||||
# 下载程序资源,$1: 后端版本路径
|
||||
function install_backend_and_download_resources() {
|
||||
# 清理临时目录,上次安装失败可能有残留
|
||||
rm -rf /tmp/*
|
||||
# 更新后端程序
|
||||
if ! download_and_unzip "${GITHUB_PROXY}https://github.com/jxxghp/MoviePilot/archive/refs/${1}" "App"; then
|
||||
WARN "后端程序下载失败,继续使用旧的程序来启动..."
|
||||
@@ -61,16 +69,33 @@ function install_backend_and_download_resources() {
|
||||
ERROR "pip 更新失败,请重新拉取镜像"
|
||||
return 1
|
||||
fi
|
||||
if ! pip install ${PIP_OPTIONS} --root-user-action=ignore -r /tmp/App/requirements.txt > /dev/null; then
|
||||
if ! pip install ${PIP_OPTIONS} --root-user-action=ignore -r ${TMP_PATH}/App/requirements.txt > /dev/null; then
|
||||
ERROR "安装依赖失败,请重新拉取镜像"
|
||||
return 1
|
||||
fi
|
||||
INFO "安装依赖成功"
|
||||
# 从后端文件中读取前端版本号
|
||||
frontend_version=$(sed -n "s/^FRONTEND_VERSION\s*=\s*'\([^']*\)'/\1/p" /tmp/App/version.py)
|
||||
if [[ "${frontend_version}" != *v* ]]; then
|
||||
WARN "前端最新版本号获取失败,继续启动..."
|
||||
return 1
|
||||
# 如果是"heads/v2.zip",则查找v2开头的最新版本号
|
||||
if [[ "${1}" == "heads/v2.zip" ]]; then
|
||||
INFO "正在获取前端最新版本号..."
|
||||
# 获取所有发布的版本列表,并筛选出以v2开头的版本号
|
||||
releases=$(curl ${CURL_OPTIONS} "https://api.github.com/repos/jxxghp/MoviePilot-Frontend/releases" ${CURL_HEADERS} | jq -r '.[].tag_name' | grep "^v2\.")
|
||||
if [ -z "$releases" ]; then
|
||||
WARN "未找到任何v2前端版本,继续启动..."
|
||||
return 1
|
||||
else
|
||||
# 找到最新的v2版本
|
||||
frontend_version=$(echo "$releases" | sort -V | tail -n 1)
|
||||
fi
|
||||
INFO "前端最新版本号:${frontend_version}"
|
||||
else
|
||||
INFO "正在获取前端版本号..."
|
||||
# 从后端文件中读取前端版本号
|
||||
frontend_version=$(sed -n "s/^FRONTEND_VERSION\s*=\s*'\([^']*\)'/\1/p" ${TMP_PATH}/App/version.py)
|
||||
if [[ "${frontend_version}" != *v* ]]; then
|
||||
WARN "前端版本号获取失败,继续启动..."
|
||||
return 1
|
||||
fi
|
||||
INFO "前端版本号:${frontend_version}"
|
||||
fi
|
||||
# 更新前端程序
|
||||
if ! download_and_unzip "${GITHUB_PROXY}https://github.com/jxxghp/MoviePilot-Frontend/releases/download/${frontend_version}/dist.zip" "dist"; then
|
||||
@@ -94,11 +119,11 @@ function install_backend_and_download_resources() {
|
||||
rm -rf /app
|
||||
mkdir -p /app
|
||||
# 复制新后端程序
|
||||
cp -a /tmp/App/* /app/
|
||||
cp -a ${TMP_PATH}/App/* /app/
|
||||
# 复制新前端程序
|
||||
rm -rf /public
|
||||
mkdir -p /public
|
||||
cp -a /tmp/dist/* /public/
|
||||
cp -a ${TMP_PATH}/dist/* /public/
|
||||
INFO "程序部分更新成功,前端版本:${frontend_version},后端版本:${1}"
|
||||
# 恢复插件目录
|
||||
cp -a /plugins/* /app/app/plugins/
|
||||
@@ -112,10 +137,10 @@ function install_backend_and_download_resources() {
|
||||
fi
|
||||
INFO "站点资源下载成功"
|
||||
# 复制新站点资源
|
||||
cp -a /tmp/Resources/resources/* /app/app/helper/
|
||||
cp -a ${TMP_PATH}/Resources/resources/* /app/app/helper/
|
||||
INFO "站点资源更新成功"
|
||||
# 清理临时目录
|
||||
rm -rf /tmp/*
|
||||
rm -rf "${TMP_PATH}"
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -287,11 +312,11 @@ if [[ "${MOVIEPILOT_AUTO_UPDATE}" = "true" ]] || [[ "${MOVIEPILOT_AUTO_UPDATE}"
|
||||
# 获取所有发布的版本列表,并筛选出以v2开头的版本号
|
||||
releases=$(curl ${CURL_OPTIONS} "https://api.github.com/repos/jxxghp/MoviePilot/releases" ${CURL_HEADERS} | jq -r '.[].tag_name' | grep "^v2\.")
|
||||
if [ -z "$releases" ]; then
|
||||
WARN "未找到任何v2.x版本,继续启动..."
|
||||
WARN "未找到任何v2后端版本,继续启动..."
|
||||
else
|
||||
# 找到最新的v2版本
|
||||
latest_v2=$(echo "$releases" | sort -V | tail -n 1)
|
||||
INFO "最新的v2.x版本号:${latest_v2}"
|
||||
INFO "最新的v2后端版本号:${latest_v2}"
|
||||
# 使用版本号比较函数进行比较,并下载最新版本
|
||||
compare_versions "${current_version}" "${latest_v2}"
|
||||
fi
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
APP_VERSION = 'v2.0.4'
|
||||
FRONTEND_VERSION = 'v2.0.4'
|
||||
APP_VERSION = 'v2.0.7'
|
||||
FRONTEND_VERSION = 'v2.0.7'
|
||||
|
||||
Reference in New Issue
Block a user