Compare commits

...

59 Commits

Author SHA1 Message Date
jxxghp
cb4981adb3 v2.0.7
- 修复了手动整理强制目录的问题
- 修复了AList无法整理文件的问题
- 修复了下载种子不使用全局UA的问题
- 修复了幼儿园的索引
- 修复了一处资源类型识别错误
- 用户认证现在也可以通过UI完成了
2024-11-19 20:42:25 +08:00
jxxghp
6880b42a84 fix #3161 2024-11-19 20:38:06 +08:00
jxxghp
97054adc61 fix 手动整理时强制目录 2024-11-19 20:22:31 +08:00
jxxghp
de94e5d595 fix #3166 2024-11-19 20:12:27 +08:00
jxxghp
a5a734d091 fix u115 transtype 2024-11-19 18:04:48 +08:00
jxxghp
efb607d22f Merge remote-tracking branch 'origin/v2' into v2 2024-11-19 13:31:52 +08:00
jxxghp
d0b2787a7c fix #1832 2024-11-19 13:11:54 +08:00
jxxghp
d5988ff443 Merge pull request #3165 from InfinityPacer/feature/module 2024-11-19 12:24:37 +08:00
InfinityPacer
96b4f1b575 feat(site): set default site timeout to 15 seconds 2024-11-19 11:10:01 +08:00
jxxghp
bb6b8439c7 fix siteauth scheduler 2024-11-19 08:39:39 +08:00
jxxghp
9cdce4509d fix siteauth schema 2024-11-19 08:25:12 +08:00
jxxghp
3956ab1fe8 add siteauth api 2024-11-19 08:18:26 +08:00
jxxghp
14686fdb03 合并拉取请求 #3159
fix: 去除资源搜索中多余的`订阅附加参数`过滤
2024-11-18 23:25:03 +08:00
Attente
32892ab747 fix: 去除资源搜索中多余的订阅附加参数过滤 2024-11-18 17:03:49 +08:00
jxxghp
79c637e003 fix #3154 相同事件避免并发处理 2024-11-18 08:01:43 +08:00
jxxghp
d7c260715a fix 115 2024-11-17 21:22:47 +08:00
jxxghp
2dfb089a39 fix bug 2024-11-17 21:04:24 +08:00
jxxghp
e04179525b Merge pull request #3146 from InfinityPacer/feature/module
chore(qbittorrent): update qbittorrent-api to version 2024.11.69
2024-11-17 15:59:43 +08:00
jxxghp
d044364c68 fix 115扫码后要重启 2024-11-17 15:58:29 +08:00
InfinityPacer
a0f912ffbe chore(qbittorrent): update qbittorrent-api to version 2024.11.69 2024-11-17 15:43:06 +08:00
jxxghp
d7c8b08d7a fix 115 2024-11-17 15:23:30 +08:00
jxxghp
f752082e1b v2.0.6 2024-11-17 15:15:42 +08:00
jxxghp
201ec21adf 优化Dev更新最新前端 2024-11-17 15:14:00 +08:00
jxxghp
57590323b2 fix ext 2024-11-17 14:56:42 +08:00
jxxghp
4636c7ada7 fix #3141 2024-11-17 14:14:13 +08:00
jxxghp
4c86a4da5f fix alist token 2024-11-17 14:07:39 +08:00
jxxghp
8dc9acf071 fix 115 2024-11-17 14:03:03 +08:00
jxxghp
abebae3664 Merge pull request #3139 from wdmcheng/v2 2024-11-17 12:00:41 +08:00
wdmcheng
4f7d8866a0 fix 本地存储 upload 后将文件识别为文件夹的问题 2024-11-17 11:50:33 +08:00
jxxghp
cceb22d729 fix log level 2024-11-17 08:56:02 +08:00
jxxghp
89edbb93f5 fix #3135 2024-11-17 08:52:15 +08:00
jxxghp
4ffb406172 更新 requirements.in 2024-11-17 02:23:07 +08:00
jxxghp
293e417865 feat:切换使用python-115 2024-11-17 02:10:45 +08:00
jxxghp
510c20dc70 fix 2024-11-16 21:49:54 +08:00
jxxghp
8e1810955b fix #3082 2024-11-16 20:56:32 +08:00
jxxghp
73f732fe1d fix #3126 目录删除加固 2024-11-16 20:29:17 +08:00
jxxghp
d6f5160959 fix mteam 消息99999 2024-11-16 19:55:41 +08:00
jxxghp
d64a7086dd fix #3120 2024-11-16 13:32:58 +08:00
jxxghp
825d9b768f 更新 version.py 2024-11-16 11:18:23 +08:00
jxxghp
f758a47f4f Merge pull request #3122 from DDS-Derek/fix_update 2024-11-16 11:02:04 +08:00
jxxghp
fc69d7e6c1 fix 2024-11-16 10:55:17 +08:00
DDSRem
edc30266c8 fix(update): clear tmp directory causes data loss
fix https://github.com/jxxghp/MoviePilot/issues/2996
2024-11-16 10:53:33 +08:00
jxxghp
665da9dad3 Merge pull request #3121 from DDS-Derek/fix_nginx 2024-11-16 10:37:23 +08:00
DDSRem
4048acf60e feat(docker): nginx client_max_body_size configuration
fix https://github.com/jxxghp/MoviePilot/issues/2951
fix https://github.com/jxxghp/MoviePilot/issues/2720
2024-11-16 10:23:28 +08:00
jxxghp
f116229ecc fix #3108 2024-11-16 09:50:55 +08:00
jxxghp
f6a2efb256 fix #3116 2024-11-16 09:25:46 +08:00
jxxghp
af3a50f7ea feat:订阅支持绑定下载器 2024-11-16 09:00:18 +08:00
jxxghp
44a0e5b4a7 fix #3120 2024-11-16 08:41:30 +08:00
jxxghp
f40a1246ff Merge pull request #3118 from wikrin/database 2024-11-16 07:54:53 +08:00
jxxghp
dd890c410c Merge pull request #3117 from wikrin/site 2024-11-16 07:54:42 +08:00
Attente
8fd7f2c875 fix 资源搜索下载时设置的下载器不生效的问题 2024-11-16 01:44:20 +08:00
Attente
8c09b3482f Upgrade the database 2024-11-16 00:28:13 +08:00
Attente
0066247a2b feat: 站点管理增加下载器选择 2024-11-16 00:22:04 +08:00
jxxghp
c7926fc575 Merge pull request #3113 from InfinityPacer/feature/module 2024-11-15 21:59:50 +08:00
InfinityPacer
ac5b9fd4e5 fix(rclone): specify UTF-8 encoding when save config 2024-11-15 17:42:11 +08:00
jxxghp
42dc539df6 fix #3013 2024-11-15 16:17:51 +08:00
jxxghp
e60d785a11 fix meta re 2024-11-15 13:50:33 +08:00
jxxghp
33558d6197 Merge pull request #3102 from InfinityPacer/feature/module 2024-11-15 12:01:21 +08:00
InfinityPacer
46d2ffeb75 fix #3100 2024-11-15 09:08:32 +08:00
43 changed files with 570 additions and 413 deletions

View File

@@ -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} 刮削完成")

View File

@@ -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,

View File

@@ -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:

View File

@@ -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

View File

@@ -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)

View File

@@ -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]:
"""
获取媒体服务器最新入库条目
"""

View File

@@ -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

View File

@@ -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)

View File

@@ -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"),

View File

@@ -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} 个文件,"

View File

@@ -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
# 订阅模式

View File

@@ -23,6 +23,8 @@ class TorrentInfo:
site_proxy: bool = False
# 站点优先级
site_order: int = 0
# 站点下载器
site_downloader: str = None
# 种子名称
title: str = None
# 种子副标题

View File

@@ -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)

View File

@@ -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"

View File

@@ -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

View File

@@ -64,6 +64,8 @@ class Subscribe(Base):
username = Column(String)
# 订阅站点
sites = Column(JSON, default=list)
# 下载器
downloader = Column(String)
# 是否洗版
best_version = Column(Integer, default=0)
# 当前优先级

View File

@@ -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)

View File

@@ -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, ""

View File

@@ -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

View File

@@ -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)

View File

@@ -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]:
"""

View File

@@ -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:
"""

View File

@@ -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

View File

@@ -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()

View File

@@ -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: 站点配置

View File

@@ -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:

View File

@@ -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,

View File

@@ -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,

View File

@@ -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]

View File

@@ -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()))

View File

@@ -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

View File

@@ -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
# 种子副标题

View File

@@ -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]]] = {}

View File

@@ -54,6 +54,8 @@ class Subscribe(BaseModel):
username: Optional[str] = None
# 订阅站点
sites: Optional[List[int]] = []
# 下载器
downloader: Optional[str] = None
# 是否洗版
best_version: Optional[int] = 0
# 当前优先级

View File

@@ -122,6 +122,8 @@ class SystemConfigKey(Enum):
DefaultMovieSubscribeConfig = "DefaultMovieSubscribeConfig"
# 默认电视剧订阅规则
DefaultTvSubscribeConfig = "DefaultTvSubscribeConfig"
# 用户站点认证参数
UserSiteAuthParams = "UserSiteAuthParams"
# 处理进度Key字典

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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;

View File

@@ -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

View File

@@ -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
View File

@@ -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

View File

@@ -1,2 +1,2 @@
APP_VERSION = 'v2.0.4'
FRONTEND_VERSION = 'v2.0.4'
APP_VERSION = 'v2.0.7'
FRONTEND_VERSION = 'v2.0.7'