mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-05-08 09:13:15 +08:00
Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe25f8f48f | ||
|
|
7f59572d8b | ||
|
|
90fc4c6bad | ||
|
|
bcbfe2ccd5 | ||
|
|
bd9a1d7ec7 | ||
|
|
9331ba64d6 | ||
|
|
21e5cb0a03 | ||
|
|
1a8e0c9ecb | ||
|
|
16fc0d31cd | ||
|
|
a622ada58b | ||
|
|
ee9c4948d3 | ||
|
|
cf28e1d963 | ||
|
|
089ec36160 | ||
|
|
04ce774c22 | ||
|
|
99c1422f37 | ||
|
|
b583a60f23 | ||
|
|
7be2910809 | ||
|
|
30de524319 | ||
|
|
c431d5e759 | ||
|
|
184b62b024 | ||
|
|
2751770350 | ||
|
|
75d98aee8e | ||
|
|
48120b9406 | ||
|
|
0e302d7959 | ||
|
|
59cd176f44 | ||
|
|
619f728f09 | ||
|
|
6e8002acc4 | ||
|
|
8a4a6174f7 | ||
|
|
ee6c4823d3 | ||
|
|
14dcb73d06 | ||
|
|
e15107e5ec | ||
|
|
0167a9462e | ||
|
|
7fa1d342ab | ||
|
|
05b9988e1d | ||
|
|
1c09e61219 | ||
|
|
35f0ad7a83 | ||
|
|
7ae1d6763a | ||
|
|
460e859795 | ||
|
|
4b88ec6460 | ||
|
|
27ee13bb7e | ||
|
|
e6cdd337c3 | ||
|
|
7d8dd12131 | ||
|
|
0800e3a136 | ||
|
|
9b0f1a2a04 | ||
|
|
9de3cb0f92 | ||
|
|
c053a8291c | ||
|
|
a0ddfe173b | ||
|
|
17843a7c71 | ||
|
|
324ae5c883 | ||
|
|
ef03989c3f | ||
|
|
63412ddd42 | ||
|
|
30ce32608a | ||
|
|
74799ad096 | ||
|
|
31176f99c8 | ||
|
|
b9439c05ec | ||
|
|
435a04da0c | ||
|
|
0040b266a5 | ||
|
|
645de137f2 | ||
|
|
1883607118 | ||
|
|
4ccae1dac7 | ||
|
|
ff75db310f | ||
|
|
5788520401 | ||
|
|
570dddc120 | ||
|
|
ea31072ae5 | ||
|
|
5eca5a6011 | ||
|
|
67d5357227 | ||
|
|
a0d04ff488 | ||
|
|
f83787508f | ||
|
|
20aba7eb17 | ||
|
|
0cdea3318c | ||
|
|
4dc2c18075 | ||
|
|
74e97abac4 |
15
.github/workflows/build.yml
vendored
15
.github/workflows/build.yml
vendored
@@ -56,10 +56,22 @@ jobs:
|
||||
cache-from: type=gha, scope=${{ github.workflow }}-docker
|
||||
cache-to: type=gha, scope=${{ github.workflow }}-docker
|
||||
|
||||
- name: Get existing release body
|
||||
id: get_release_body
|
||||
continue-on-error: true
|
||||
run: |
|
||||
release_body=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/releases/tags/v${{ env.app_version }}" | \
|
||||
jq -r '.body // ""')
|
||||
echo "RELEASE_BODY<<EOF" >> $GITHUB_ENV
|
||||
echo "$release_body" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Delete Release
|
||||
uses: dev-drprasad/delete-tag-and-release@v1.1
|
||||
continue-on-error: true
|
||||
with:
|
||||
tag_name: ${{ env.app_version }}
|
||||
tag_name: v${{ env.app_version }}
|
||||
delete_release: true
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -68,6 +80,7 @@ jobs:
|
||||
with:
|
||||
tag_name: v${{ env.app_version }}
|
||||
name: v${{ env.app_version }}
|
||||
body: ${{ env.RELEASE_BODY }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
make_latest: false
|
||||
|
||||
@@ -9,7 +9,7 @@ from app import schemas
|
||||
from app.command import Command
|
||||
from app.core.config import settings
|
||||
from app.core.plugin import PluginManager
|
||||
from app.core.security import verify_apikey, verify_token, verify_apitoken
|
||||
from app.core.security import verify_apikey, verify_token
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.db.user_oper import get_current_active_superuser
|
||||
from app.factory import app
|
||||
@@ -68,9 +68,13 @@ def _update_plugin_api_routes(plugin_id: Optional[str], action: str):
|
||||
try:
|
||||
api["path"] = api_path
|
||||
allow_anonymous = api.pop("allow_anonymous", False)
|
||||
auth_mode = api.pop("auth", "apikey")
|
||||
dependencies = api.setdefault("dependencies", [])
|
||||
if not allow_anonymous and Depends(verify_apikey) not in dependencies:
|
||||
dependencies.append(Depends(verify_apikey))
|
||||
if not allow_anonymous:
|
||||
if auth_mode == "bear" and Depends(verify_token) not in dependencies:
|
||||
dependencies.append(Depends(verify_token))
|
||||
elif Depends(verify_apikey) not in dependencies:
|
||||
dependencies.append(Depends(verify_apikey))
|
||||
app.add_api_route(**api, tags=["plugin"])
|
||||
is_modified = True
|
||||
logger.debug(f"Added plugin route: {api_path}")
|
||||
@@ -118,6 +122,18 @@ def _clean_protected_routes(existing_paths: dict):
|
||||
logger.error(f"Error removing protected route {protected_route}: {str(e)}")
|
||||
|
||||
|
||||
def register_plugin(plugin_id: str):
|
||||
"""
|
||||
注册一个插件相关的服务
|
||||
"""
|
||||
# 注册插件服务
|
||||
Scheduler().update_plugin_job(plugin_id)
|
||||
# 注册菜单命令
|
||||
Command().init_commands(plugin_id)
|
||||
# 注册插件API
|
||||
register_plugin_api(plugin_id)
|
||||
|
||||
|
||||
@router.get("/", summary="所有插件", response_model=List[schemas.Plugin])
|
||||
def all_plugins(_: schemas.TokenPayload = Depends(get_current_active_superuser),
|
||||
state: Optional[str] = "all") -> List[schemas.Plugin]:
|
||||
@@ -181,6 +197,18 @@ def statistic(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
return PluginHelper().get_statistic()
|
||||
|
||||
|
||||
@router.get("/reload/{plugin_id}", summary="重新加载插件", response_model=schemas.Response)
|
||||
def reload_plugin(plugin_id: str, _: schemas.TokenPayload = Depends(get_current_active_superuser)) -> Any:
|
||||
"""
|
||||
重新加载插件
|
||||
"""
|
||||
# 重新加载插件
|
||||
PluginManager().reload_plugin(plugin_id)
|
||||
# 注册插件服务
|
||||
register_plugin(plugin_id)
|
||||
return schemas.Response(success=True)
|
||||
|
||||
|
||||
@router.get("/install/{plugin_id}", summary="安装插件", response_model=schemas.Response)
|
||||
def install(plugin_id: str,
|
||||
repo_url: Optional[str] = "",
|
||||
@@ -209,14 +237,8 @@ def install(plugin_id: str,
|
||||
install_plugins.append(plugin_id)
|
||||
# 保存设置
|
||||
SystemConfigOper().set(SystemConfigKey.UserInstalledPlugins, install_plugins)
|
||||
# 加载插件到内存
|
||||
PluginManager().reload_plugin(plugin_id)
|
||||
# 注册插件服务
|
||||
Scheduler().update_plugin_job(plugin_id)
|
||||
# 注册菜单命令
|
||||
Command().init_commands(plugin_id)
|
||||
# 注册插件API
|
||||
register_plugin_api(plugin_id)
|
||||
# 重新加载插件
|
||||
reload_plugin(plugin_id)
|
||||
return schemas.Response(success=True)
|
||||
|
||||
|
||||
@@ -312,14 +334,8 @@ def reset_plugin(plugin_id: str,
|
||||
PluginManager().delete_plugin_config(plugin_id)
|
||||
# 删除插件所有数据
|
||||
PluginManager().delete_plugin_data(plugin_id)
|
||||
# 重新生效插件
|
||||
PluginManager().reload_plugin(plugin_id)
|
||||
# 注册插件服务
|
||||
Scheduler().update_plugin_job(plugin_id)
|
||||
# 注册菜单命令
|
||||
Command().init_commands(plugin_id)
|
||||
# 注册插件API
|
||||
register_plugin_api(plugin_id)
|
||||
# 重新加载插件
|
||||
reload_plugin(plugin_id)
|
||||
return schemas.Response(success=True)
|
||||
|
||||
|
||||
@@ -378,11 +394,7 @@ def set_plugin_config(plugin_id: str, conf: dict,
|
||||
# 重新生效插件
|
||||
PluginManager().init_plugin(plugin_id, conf)
|
||||
# 注册插件服务
|
||||
Scheduler().update_plugin_job(plugin_id)
|
||||
# 注册菜单命令
|
||||
Command().init_commands(plugin_id)
|
||||
# 注册插件API
|
||||
register_plugin_api(plugin_id)
|
||||
register_plugin(plugin_id)
|
||||
return schemas.Response(success=True)
|
||||
|
||||
|
||||
@@ -407,7 +419,3 @@ def uninstall_plugin(plugin_id: str,
|
||||
# 移除插件
|
||||
PluginManager().remove_plugin(plugin_id)
|
||||
return schemas.Response(success=True)
|
||||
|
||||
|
||||
# 注册全部插件API
|
||||
register_plugin_api()
|
||||
|
||||
@@ -7,6 +7,7 @@ from starlette.background import BackgroundTasks
|
||||
from app import schemas
|
||||
from app.chain.site import SiteChain
|
||||
from app.chain.torrents import TorrentsChain
|
||||
from app.command import Command
|
||||
from app.core.event import EventManager
|
||||
from app.core.plugin import PluginManager
|
||||
from app.core.security import verify_token
|
||||
@@ -22,6 +23,7 @@ from app.helper.sites import SitesHelper
|
||||
from app.scheduler import Scheduler
|
||||
from app.schemas.types import SystemConfigKey, EventType
|
||||
from app.utils.string import StringUtils
|
||||
from startup.plugins_initializer import register_plugin_api
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -385,8 +387,11 @@ def auth_site(
|
||||
return schemas.Response(success=False, message="请输入认证站点和认证参数")
|
||||
status, msg = SitesHelper().check_user(auth_info.site, auth_info.params)
|
||||
SystemConfigOper().set(SystemConfigKey.UserSiteAuthParams, auth_info.dict())
|
||||
# 认证成功后,重新初始化插件
|
||||
PluginManager().init_config()
|
||||
Scheduler().init_plugin_jobs()
|
||||
Command().init_commands()
|
||||
register_plugin_api()
|
||||
return schemas.Response(success=status, message=msg)
|
||||
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ def qrcode(name: str, _: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
|
||||
|
||||
@router.get("/check/{name}", summary="二维码登录确认", response_model=schemas.Response)
|
||||
def check(name: str, ck: Optional[str] = None, t: Optional[str] = None,
|
||||
def check(name: str, ck: Optional[str] = None, t: Optional[str] = None,
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
二维码登录确认
|
||||
@@ -56,6 +56,16 @@ def save(name: str,
|
||||
return schemas.Response(success=True)
|
||||
|
||||
|
||||
@router.get("/reset/{name}", summary="重置存储配置", response_model=schemas.Response)
|
||||
def reset(name: str,
|
||||
_: User = Depends(get_current_active_superuser)) -> Any:
|
||||
"""
|
||||
重置存储配置
|
||||
"""
|
||||
StorageChain().reset_config(name)
|
||||
return schemas.Response(success=True)
|
||||
|
||||
|
||||
@router.post("/list", summary="所有目录和文件", response_model=List[schemas.FileItem])
|
||||
def list_files(fileitem: schemas.FileItem,
|
||||
sort: Optional[str] = 'updated_at',
|
||||
|
||||
@@ -339,7 +339,8 @@ class DownloadChain(ChainBase):
|
||||
meta=_meta,
|
||||
mediainfo=_media,
|
||||
torrentinfo=_torrent,
|
||||
download_episodes=download_episodes
|
||||
download_episodes=download_episodes,
|
||||
username=username,
|
||||
)
|
||||
# 下载成功后处理
|
||||
self.download_added(context=context, download_dir=download_dir, torrent_path=torrent_file)
|
||||
|
||||
@@ -449,23 +449,19 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
||||
# 生成目录内图片文件
|
||||
if init_folder:
|
||||
# 图片
|
||||
for attr_name, attr_value in vars(mediainfo).items():
|
||||
if attr_value \
|
||||
and attr_name.endswith("_path") \
|
||||
and attr_value \
|
||||
and isinstance(attr_value, str) \
|
||||
and attr_value.startswith("http"):
|
||||
image_name = attr_name.replace("_path", "") + Path(attr_value).suffix
|
||||
image_path = filepath / image_name
|
||||
image_dict = self.metadata_img(mediainfo=mediainfo)
|
||||
if image_dict:
|
||||
for image_name, image_url in image_dict.items():
|
||||
image_path = filepath.with_name(image_name)
|
||||
if overwrite or not self.storagechain.get_file_item(storage=fileitem.storage,
|
||||
path=image_path):
|
||||
# 下载图片
|
||||
content = __download_image(_url=attr_value)
|
||||
content = __download_image(image_url)
|
||||
# 写入图片到当前目录
|
||||
if content:
|
||||
__save_file(_fileitem=fileitem, _path=image_path, _content=content)
|
||||
else:
|
||||
logger.info(f"已存在图片文件:{image_path}")
|
||||
else:
|
||||
logger.info(f"已存在图片文件:{image_path}")
|
||||
else:
|
||||
# 电视剧
|
||||
if fileitem.type == "file":
|
||||
|
||||
@@ -119,7 +119,7 @@ class MessageChain(ChainBase):
|
||||
userid = info.userid
|
||||
# 用户名
|
||||
username = info.username or userid
|
||||
if not userid:
|
||||
if userid is None or userid == '':
|
||||
logger.debug(f'未识别到用户ID:{body}{form}{args}')
|
||||
return
|
||||
# 消息内容
|
||||
|
||||
@@ -24,6 +24,12 @@ class StorageChain(ChainBase):
|
||||
"""
|
||||
self.run_module("save_config", storage=storage, conf=conf)
|
||||
|
||||
def reset_config(self, storage: str) -> None:
|
||||
"""
|
||||
重置存储配置
|
||||
"""
|
||||
self.run_module("reset_config", storage=storage)
|
||||
|
||||
def generate_qrcode(self, storage: str) -> Optional[Tuple[dict, str]]:
|
||||
"""
|
||||
生成二维码
|
||||
@@ -131,28 +137,43 @@ class StorageChain(ChainBase):
|
||||
"""
|
||||
删除媒体文件,以及不含媒体文件的目录
|
||||
"""
|
||||
|
||||
def __is_bluray_dir(_fileitem: schemas.FileItem) -> bool:
|
||||
"""
|
||||
检查是否蓝光目录
|
||||
"""
|
||||
_dir_files = self.list_files(fileitem=_fileitem, recursion=False)
|
||||
if _dir_files:
|
||||
for _f in _dir_files:
|
||||
if _f.type == "dir" and _f.name in ["BDMV", "CERTIFICATE"]:
|
||||
return True
|
||||
return False
|
||||
|
||||
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":
|
||||
# 本身是目录
|
||||
if _blue_dir := self.list_files(fileitem=fileitem, recursion=False):
|
||||
# 删除蓝光目录
|
||||
for _f in _blue_dir:
|
||||
if _f.type == "dir" and _f.name in ["BDMV", "CERTIFICATE"]:
|
||||
logger.warn(f"【{fileitem.storage}】{_f.path} 删除蓝光目录")
|
||||
self.delete_file(_f)
|
||||
if self.any_files(fileitem, extensions=media_exts) is False:
|
||||
logger.warn(f"【{fileitem.storage}】{fileitem.path} 不存在其它媒体文件,删除空目录")
|
||||
return self.delete_file(fileitem)
|
||||
return False
|
||||
if __is_bluray_dir(fileitem):
|
||||
logger.warn(f"正在删除蓝光原盘目录:【{fileitem.storage}】{fileitem.path}")
|
||||
if not self.delete_file(fileitem):
|
||||
logger.warn(f"【{fileitem.storage}】{fileitem.path} 删除失败")
|
||||
return False
|
||||
elif self.any_files(fileitem, extensions=media_exts) is False:
|
||||
logger.warn(f"【{fileitem.storage}】{fileitem.path} 不存在其它媒体文件,正在删除空目录")
|
||||
if not self.delete_file(fileitem):
|
||||
logger.warn(f"【{fileitem.storage}】{fileitem.path} 删除失败")
|
||||
return False
|
||||
# 不处理父目录
|
||||
return True
|
||||
elif delete_self:
|
||||
# 本身是文件
|
||||
logger.warn(f"正在删除【{fileitem.storage}】{fileitem.path}")
|
||||
# 本身是文件,需要删除文件
|
||||
logger.warn(f"正在删除文件【{fileitem.storage}】{fileitem.path}")
|
||||
if not self.delete_file(fileitem):
|
||||
logger.warn(f"【{fileitem.storage}】{fileitem.path} 删除失败")
|
||||
return False
|
||||
|
||||
if mtype:
|
||||
# 重命名格式
|
||||
rename_format = settings.TV_RENAME_FORMAT \
|
||||
@@ -161,11 +182,14 @@ class StorageChain(ChainBase):
|
||||
rename_format_level = len(rename_format.split("/")) - 1
|
||||
if rename_format_level < 1:
|
||||
return True
|
||||
# 处理上级目录
|
||||
# 处理媒体文件根目录
|
||||
dir_item = self.get_file_item(storage=fileitem.storage,
|
||||
path=Path(fileitem.path).parents[rename_format_level - 1])
|
||||
else:
|
||||
# 处理上级目录
|
||||
dir_item = self.get_parent_item(fileitem)
|
||||
|
||||
# 检查和删除上级目录
|
||||
if dir_item and len(Path(dir_item.path).parts) > 2:
|
||||
# 如何目录是所有下载目录、媒体库目录的上级,则不处理
|
||||
for d in self.directoryhelper.get_dirs():
|
||||
@@ -177,7 +201,9 @@ class StorageChain(ChainBase):
|
||||
return True
|
||||
# 不存在其他媒体文件,删除空目录
|
||||
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)
|
||||
logger.warn(f"【{dir_item.storage}】{dir_item.path} 不存在其它媒体文件,正在删除空目录")
|
||||
if not self.delete_file(dir_item):
|
||||
logger.warn(f"【{dir_item.storage}】{dir_item.path} 删除失败")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -241,6 +241,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
link=link,
|
||||
username=username
|
||||
),
|
||||
meta=metainfo,
|
||||
mediainfo=mediainfo,
|
||||
username=username
|
||||
)
|
||||
@@ -1023,7 +1024,8 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
),
|
||||
meta=meta,
|
||||
mediainfo=mediainfo,
|
||||
msgstr=msgstr
|
||||
msgstr=msgstr,
|
||||
username=subscribe.username
|
||||
)
|
||||
# 发送事件
|
||||
EventManager().send_event(EventType.SubscribeComplete, {
|
||||
|
||||
5
app/chain/transfer.py
Normal file → Executable file
5
app/chain/transfer.py
Normal file → Executable file
@@ -860,7 +860,7 @@ class TransferChain(ChainBase, metaclass=Singleton):
|
||||
|
||||
# 设置下载任务状态
|
||||
if state:
|
||||
self.transfer_completed(hashs=torrent.hash)
|
||||
self.transfer_completed(hashs=torrent.hash, downloader=torrent.downloader)
|
||||
|
||||
# 结束
|
||||
logger.info("所有下载器中下载完成的文件已整理完成")
|
||||
@@ -1385,5 +1385,6 @@ class TransferChain(ChainBase, metaclass=Singleton):
|
||||
meta=meta,
|
||||
mediainfo=mediainfo,
|
||||
transferinfo=transferinfo,
|
||||
season_episode=season_episode
|
||||
season_episode=season_episode,
|
||||
username=username
|
||||
)
|
||||
|
||||
@@ -103,6 +103,8 @@ class ConfigModel(BaseModel):
|
||||
TMDB_API_DOMAIN: str = "api.themoviedb.org"
|
||||
# TMDB元数据语言
|
||||
TMDB_LOCALE: str = "zh"
|
||||
# 刮削使用TMDB原始语种图片
|
||||
TMDB_SCRAP_ORIGINAL_IMAGE: bool = False
|
||||
# TMDB API Key
|
||||
TMDB_API_KEY: str = "db55323b8d3e4154498498a75642b381"
|
||||
# TVDB API Key
|
||||
@@ -215,7 +217,8 @@ class ConfigModel(BaseModel):
|
||||
"https://github.com/thsrite/MoviePilot-Plugins,"
|
||||
"https://github.com/honue/MoviePilot-Plugins,"
|
||||
"https://github.com/InfinityPacer/MoviePilot-Plugins,"
|
||||
"https://github.com/DDS-Derek/MoviePilot-Plugins")
|
||||
"https://github.com/DDS-Derek/MoviePilot-Plugins,"
|
||||
"https://github.com/madrays/MoviePilot-Plugins")
|
||||
# 插件安装数据共享
|
||||
PLUGIN_STATISTIC_SHARE: bool = True
|
||||
# 是否开启插件热加载
|
||||
@@ -550,6 +553,7 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
|
||||
return {
|
||||
"server": self.PROXY_HOST
|
||||
}
|
||||
return None
|
||||
|
||||
@property
|
||||
def GITHUB_HEADERS(self):
|
||||
|
||||
@@ -204,18 +204,21 @@ class PluginManager(metaclass=Singleton):
|
||||
# 停止插件
|
||||
if pid:
|
||||
logger.info(f"正在停止插件 {pid}...")
|
||||
plugin_obj = self._running_plugins.get(pid)
|
||||
if not plugin_obj:
|
||||
logger.warning(f"插件 {pid} 不存在或未加载")
|
||||
return
|
||||
plugins = {pid: plugin_obj}
|
||||
else:
|
||||
logger.info("正在停止所有插件...")
|
||||
for plugin_id, plugin in self._running_plugins.items():
|
||||
if pid and plugin_id != pid:
|
||||
continue
|
||||
plugins = self._running_plugins
|
||||
for plugin_id, plugin in plugins.items():
|
||||
eventmanager.disable_event_handler(type(plugin))
|
||||
self.__stop_plugin(plugin)
|
||||
# 清空对像
|
||||
if pid:
|
||||
# 清空指定插件
|
||||
if pid in self._running_plugins:
|
||||
self._running_plugins.pop(pid)
|
||||
self._running_plugins.pop(pid, None)
|
||||
else:
|
||||
# 清空
|
||||
self._plugins = {}
|
||||
@@ -465,16 +468,20 @@ class PluginManager(metaclass=Singleton):
|
||||
}]
|
||||
"""
|
||||
ret_apis = []
|
||||
for plugin_id, plugin in self._running_plugins.items():
|
||||
if pid:
|
||||
plugins = {pid: self._running_plugins.get(pid)}
|
||||
else:
|
||||
plugins = self._running_plugins
|
||||
for plugin_id, plugin in plugins.items():
|
||||
if pid and pid != plugin_id:
|
||||
continue
|
||||
if hasattr(plugin, "get_api") and ObjectUtils.check_method(plugin.get_api):
|
||||
try:
|
||||
if not plugin.get_state():
|
||||
continue
|
||||
apis = plugin.get_api() or []
|
||||
for api in apis:
|
||||
api["path"] = f"/{plugin_id}{api['path']}"
|
||||
if not api.get("auth"):
|
||||
api["auth"] = "apikey"
|
||||
ret_apis.extend(apis)
|
||||
except Exception as e:
|
||||
logger.error(f"获取插件 {plugin_id} API出错:{str(e)}")
|
||||
@@ -633,6 +640,7 @@ class PluginManager(metaclass=Singleton):
|
||||
render_mode=render_mode,
|
||||
cols=cols or {},
|
||||
attrs=attrs or {},
|
||||
elements=elements
|
||||
)
|
||||
|
||||
def get_plugin_attr(self, pid: str, attr: str) -> Any:
|
||||
@@ -828,7 +836,8 @@ class PluginManager(metaclass=Singleton):
|
||||
logger.debug(f"获取插件是否在本地包中存在失败,{e}")
|
||||
return False
|
||||
|
||||
def get_plugins_from_market(self, market: str, package_version: Optional[str] = None) -> Optional[List[schemas.Plugin]]:
|
||||
def get_plugins_from_market(self, market: str,
|
||||
package_version: Optional[str] = None) -> Optional[List[schemas.Plugin]]:
|
||||
"""
|
||||
从指定的市场获取插件信息
|
||||
:param market: 市场的 URL 或标识
|
||||
@@ -842,7 +851,8 @@ class PluginManager(metaclass=Singleton):
|
||||
# 获取在线插件
|
||||
online_plugins = self.pluginhelper.get_plugins(market, package_version)
|
||||
if online_plugins is None:
|
||||
logger.warning(f"获取{package_version if package_version else ''}插件库失败:{market},请检查 GitHub 网络连接")
|
||||
logger.warning(
|
||||
f"获取{package_version if package_version else ''}插件库失败:{market},请检查 GitHub 网络连接")
|
||||
return []
|
||||
ret_plugins = []
|
||||
add_time = len(online_plugins)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import Column, Integer, String, Sequence, JSON
|
||||
from sqlalchemy import Column, Integer, String, Sequence, JSON, or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db import db_query, db_update, Base
|
||||
@@ -65,8 +65,11 @@ class DownloadHistory(Base):
|
||||
@staticmethod
|
||||
@db_query
|
||||
def get_by_mediaid(db: Session, tmdbid: int, doubanid: str):
|
||||
return db.query(DownloadHistory).filter(DownloadHistory.tmdbid == tmdbid,
|
||||
DownloadHistory.doubanid == doubanid).all()
|
||||
if tmdbid:
|
||||
return db.query(DownloadHistory).filter(DownloadHistory.tmdbid == tmdbid).all()
|
||||
elif doubanid:
|
||||
return db.query(DownloadHistory).filter(DownloadHistory.doubanid == doubanid).all()
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
@db_query
|
||||
@@ -81,7 +84,7 @@ class DownloadHistory(Base):
|
||||
|
||||
@staticmethod
|
||||
@db_query
|
||||
def get_last_by(db: Session, mtype: Optional[str] = None, title: Optional[str] = None,
|
||||
def get_last_by(db: Session, mtype: Optional[str] = None, title: Optional[str] = None,
|
||||
year: Optional[str] = None, season: Optional[str] = None,
|
||||
episode: Optional[str] = None, tmdbid: Optional[int] = None):
|
||||
"""
|
||||
@@ -97,18 +100,18 @@ class DownloadHistory(Base):
|
||||
DownloadHistory.type == mtype,
|
||||
DownloadHistory.seasons == season,
|
||||
DownloadHistory.episodes == episode).order_by(
|
||||
DownloadHistory.id.desc()).all()
|
||||
DownloadHistory.id.desc()).all()
|
||||
# 电视剧某季
|
||||
elif season:
|
||||
result = db.query(DownloadHistory).filter(DownloadHistory.tmdbid == tmdbid,
|
||||
DownloadHistory.type == mtype,
|
||||
DownloadHistory.seasons == season).order_by(
|
||||
DownloadHistory.id.desc()).all()
|
||||
DownloadHistory.id.desc()).all()
|
||||
else:
|
||||
# 电视剧所有季集/电影
|
||||
result = db.query(DownloadHistory).filter(DownloadHistory.tmdbid == tmdbid,
|
||||
DownloadHistory.type == mtype).order_by(
|
||||
DownloadHistory.id.desc()).all()
|
||||
DownloadHistory.id.desc()).all()
|
||||
# 标题 + 年份
|
||||
elif title and year:
|
||||
# 电视剧某季某集
|
||||
@@ -117,18 +120,18 @@ class DownloadHistory(Base):
|
||||
DownloadHistory.year == year,
|
||||
DownloadHistory.seasons == season,
|
||||
DownloadHistory.episodes == episode).order_by(
|
||||
DownloadHistory.id.desc()).all()
|
||||
DownloadHistory.id.desc()).all()
|
||||
# 电视剧某季
|
||||
elif season:
|
||||
result = db.query(DownloadHistory).filter(DownloadHistory.title == title,
|
||||
DownloadHistory.year == year,
|
||||
DownloadHistory.seasons == season).order_by(
|
||||
DownloadHistory.id.desc()).all()
|
||||
DownloadHistory.id.desc()).all()
|
||||
else:
|
||||
# 电视剧所有季集/电影
|
||||
result = db.query(DownloadHistory).filter(DownloadHistory.title == title,
|
||||
DownloadHistory.year == year).order_by(
|
||||
DownloadHistory.id.desc()).all()
|
||||
DownloadHistory.id.desc()).all()
|
||||
|
||||
if result:
|
||||
return list(result)
|
||||
|
||||
@@ -42,7 +42,7 @@ class TemplateContextBuilder:
|
||||
transferinfo: Optional[TransferInfo] = None,
|
||||
file_extension: Optional[str] = None,
|
||||
episodes_info: Optional[List[TmdbEpisode]] = None,
|
||||
include_raw_objects: bool = False,
|
||||
include_raw_objects: bool = True,
|
||||
**kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -66,13 +66,15 @@ class TemplateContextBuilder:
|
||||
if include_raw_objects:
|
||||
self._add_raw_objects(meta, mediainfo, torrentinfo, transferinfo, episodes_info)
|
||||
|
||||
return self._context
|
||||
# 移除空值
|
||||
return {k: v for k, v in self._context.items() if v is not None}
|
||||
|
||||
def _add_media_info(self, mediainfo: MediaInfo):
|
||||
"""
|
||||
增加媒体信息
|
||||
"""
|
||||
if not mediainfo: return
|
||||
season_fmt = f"S{mediainfo.season:02d}" if mediainfo.season is not None else None
|
||||
base_info = {
|
||||
# 标题
|
||||
"title": self.__convert_invalid_characters(mediainfo.title),
|
||||
@@ -80,8 +82,13 @@ class TemplateContextBuilder:
|
||||
"en_title": self.__convert_invalid_characters(mediainfo.en_title),
|
||||
# 原语种标题
|
||||
"original_title": self.__convert_invalid_characters(mediainfo.original_title),
|
||||
# 季号
|
||||
"season": self._context.get("season") or mediainfo.season,
|
||||
# Sxx
|
||||
"season_fmt": self._context.get("season_fmt") or season_fmt,
|
||||
# 年份
|
||||
"year": mediainfo.year or self._context.get("year"),
|
||||
# 媒体标题 + 年份
|
||||
"title_year": mediainfo.title_year or self._context.get("title_year"),
|
||||
}
|
||||
|
||||
@@ -145,6 +152,8 @@ class TemplateContextBuilder:
|
||||
meta.name, meta.year) if meta.year else meta.name,
|
||||
# 季号
|
||||
"season": meta.season_seq,
|
||||
# Sxx
|
||||
"season_fmt": meta.season,
|
||||
# 集号
|
||||
"episode": meta.episode_seqs,
|
||||
# 季集 SxxExx
|
||||
@@ -266,7 +275,7 @@ class TemplateContextBuilder:
|
||||
# 当前季的全部集信息
|
||||
"__episodes_info__": episodes_info,
|
||||
}
|
||||
self._context.update({k: v for k, v in raw_objects.items() if v is not None})
|
||||
self._context.update(raw_objects)
|
||||
|
||||
@staticmethod
|
||||
def __convert_invalid_characters(filename: str):
|
||||
@@ -562,6 +571,7 @@ class MessageQueueManager(metaclass=SingletonClass):
|
||||
def _parse_schedule(periods: Union[list, dict]) -> List[tuple[int, int, int, int]]:
|
||||
"""
|
||||
将字符串时间格式转换为分钟数元组
|
||||
支持格式为 'HH:MM' 或 'HH:MM:SS' 的时间字符串
|
||||
"""
|
||||
parsed = []
|
||||
if not periods:
|
||||
@@ -573,9 +583,31 @@ class MessageQueueManager(metaclass=SingletonClass):
|
||||
continue
|
||||
if not period.get('start') or not period.get('end'):
|
||||
continue
|
||||
start_h, start_m = map(int, period['start'].split(':'))
|
||||
end_h, end_m = map(int, period['end'].split(':'))
|
||||
parsed.append((start_h, start_m, end_h, end_m))
|
||||
try:
|
||||
# 处理 start 时间
|
||||
start_parts = period['start'].split(':')
|
||||
if len(start_parts) == 2:
|
||||
start_h, start_m = map(int, start_parts)
|
||||
elif len(start_parts) >= 3:
|
||||
start_h, start_m = map(int, start_parts[:2]) # 只取前两个部分 (HH:MM)
|
||||
else:
|
||||
continue
|
||||
# 处理 end 时间
|
||||
end_parts = period['end'].split(':')
|
||||
if len(end_parts) == 2:
|
||||
end_h, end_m = map(int, end_parts)
|
||||
elif len(end_parts) >= 3:
|
||||
end_h, end_m = map(int, end_parts[:2]) # 只取前两个部分 (HH:MM)
|
||||
else:
|
||||
continue
|
||||
|
||||
parsed.append((start_h, start_m, end_h, end_m))
|
||||
except ValueError as e:
|
||||
logger.error(f"解析时间周期时出现错误:{period}. 错误:{str(e)}. 跳过此周期。")
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.error(f"解析时间周期时出现意外错误:{period}. 错误:{str(e)}. 跳过此周期。")
|
||||
continue
|
||||
return parsed
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -7,14 +7,15 @@ from typing import List, Any, Callable
|
||||
|
||||
from app.log import logger
|
||||
|
||||
|
||||
FilterFuncType = Callable[[str, Any], bool]
|
||||
|
||||
|
||||
def _default_filter(name: str, obj: Any) -> bool:
|
||||
"""
|
||||
默认过滤器
|
||||
"""
|
||||
return True
|
||||
return True if name and obj else False
|
||||
|
||||
|
||||
class ModuleHelper:
|
||||
"""
|
||||
@@ -76,7 +77,8 @@ class ModuleHelper:
|
||||
|
||||
def reload_sub_modules(parent_module, parent_module_name):
|
||||
"""重新加载一级子模块"""
|
||||
for sub_importer, sub_module_name, sub_is_pkg in pkgutil.walk_packages(parent_module.__path__, parent_module_name+'.'):
|
||||
for sub_importer, sub_module_name, sub_is_pkg in pkgutil.walk_packages(parent_module.__path__,
|
||||
parent_module_name + '.'):
|
||||
try:
|
||||
full_sub_module = importlib.import_module(sub_module_name)
|
||||
importlib.reload(full_sub_module)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import sys
|
||||
import json
|
||||
import shutil
|
||||
import traceback
|
||||
@@ -455,15 +456,15 @@ class PluginHelper(metaclass=Singleton):
|
||||
:param requirements_file: 依赖的 requirements.txt 文件路径
|
||||
:return: (是否成功, 错误信息)
|
||||
"""
|
||||
base_cmd = [sys.executable, "-m", "pip", "install", "-r", str(requirements_file)]
|
||||
strategies = []
|
||||
|
||||
# 添加策略到列表中
|
||||
if settings.PIP_PROXY:
|
||||
strategies.append(("镜像站", ["pip", "install", "-r", str(requirements_file), "-i", settings.PIP_PROXY]))
|
||||
strategies.append(("镜像站", base_cmd + ["-i", settings.PIP_PROXY]))
|
||||
if settings.PROXY_HOST:
|
||||
strategies.append(
|
||||
("代理", ["pip", "install", "-r", str(requirements_file), "--proxy", settings.PROXY_HOST]))
|
||||
strategies.append(("直连", ["pip", "install", "-r", str(requirements_file)]))
|
||||
strategies.append(("代理", base_cmd + ["--proxy", settings.PROXY_HOST]))
|
||||
strategies.append(("直连", base_cmd))
|
||||
|
||||
# 遍历策略进行安装
|
||||
for strategy_name, pip_command in strategies:
|
||||
|
||||
@@ -71,3 +71,14 @@ class StorageHelper:
|
||||
config=conf
|
||||
))
|
||||
self.systemconfig.set(SystemConfigKey.Storages, [s.dict() for s in storagies])
|
||||
|
||||
def reset_storage(self, storage: str):
|
||||
"""
|
||||
重置存储配置
|
||||
"""
|
||||
storagies = self.get_storagies()
|
||||
for s in storagies:
|
||||
if s.type == storage:
|
||||
s.config = {}
|
||||
break
|
||||
self.systemconfig.set(SystemConfigKey.Storages, [s.dict() for s in storagies])
|
||||
|
||||
@@ -39,11 +39,9 @@ class DoubanModule(_ModuleBase):
|
||||
测试模块连接性
|
||||
"""
|
||||
ret = RequestUtils().get_res("https://movie.douban.com/")
|
||||
if ret and ret.status_code == 200:
|
||||
return True, ""
|
||||
elif ret:
|
||||
return False, f"无法连接豆瓣,错误码:{ret.status_code}"
|
||||
return False, "豆瓣网络连接失败"
|
||||
if ret is None:
|
||||
return False, "豆瓣网络连接失败"
|
||||
return True, ""
|
||||
|
||||
def init_setting(self) -> Tuple[str, Union[str, bool]]:
|
||||
pass
|
||||
|
||||
@@ -154,6 +154,16 @@ class FileManagerModule(_ModuleBase):
|
||||
return
|
||||
storage_oper.set_config(conf)
|
||||
|
||||
def reset_config(self, storage: str) -> None:
|
||||
"""
|
||||
重置存储配置
|
||||
"""
|
||||
storage_oper = self.__get_storage_oper(storage)
|
||||
if not storage_oper:
|
||||
logger.error(f"不支持 {storage} 的重置存储配置")
|
||||
return
|
||||
storage_oper.reset_config()
|
||||
|
||||
def generate_qrcode(self, storage: str) -> Optional[Tuple[dict, str]]:
|
||||
"""
|
||||
生成二维码
|
||||
@@ -392,6 +402,9 @@ class FileManagerModule(_ModuleBase):
|
||||
# 整理方式
|
||||
if not transfer_type:
|
||||
transfer_type = target_directory.transfer_type
|
||||
# 目标存储
|
||||
if not target_storage:
|
||||
target_storage = target_directory.library_storage
|
||||
# 是否需要重命名
|
||||
need_rename = target_directory.renaming
|
||||
# 是否需要通知
|
||||
@@ -440,6 +453,8 @@ class FileManagerModule(_ModuleBase):
|
||||
)
|
||||
# 目的操作对象
|
||||
if not target_oper:
|
||||
if not target_storage:
|
||||
target_storage = fileitem.storage
|
||||
target_oper = self.__get_storage_oper(target_storage)
|
||||
if not target_oper:
|
||||
return TransferInfo(success=False,
|
||||
@@ -895,8 +910,7 @@ class FileManagerModule(_ModuleBase):
|
||||
def __transfer_file(self, fileitem: FileItem, mediainfo: MediaInfo,
|
||||
source_oper: StorageBase, target_oper: StorageBase,
|
||||
target_storage: str, target_file: Path,
|
||||
transfer_type: str, over_flag: Optional[bool] = False) -> Tuple[
|
||||
Optional[FileItem], str]:
|
||||
transfer_type: str, over_flag: Optional[bool] = False) -> Tuple[Optional[FileItem], str]:
|
||||
"""
|
||||
整理一个文件,同时处理其他相关文件
|
||||
:param fileitem: 原文件
|
||||
@@ -1300,7 +1314,8 @@ class FileManagerModule(_ModuleBase):
|
||||
if media_files:
|
||||
for media_file in media_files:
|
||||
if f".{media_file.extension.lower()}" in settings.RMT_MEDIAEXT:
|
||||
ret_fileitems.append(media_file)
|
||||
if media_file not in ret_fileitems:
|
||||
ret_fileitems.append(media_file)
|
||||
return ret_fileitems
|
||||
|
||||
def media_exists(self, mediainfo: MediaInfo, **kwargs) -> Optional[ExistMediaInfo]:
|
||||
|
||||
@@ -61,6 +61,13 @@ class StorageBase(metaclass=ABCMeta):
|
||||
"""
|
||||
return transtype in self.transtype
|
||||
|
||||
def reset_config(self):
|
||||
"""
|
||||
重置置配置
|
||||
"""
|
||||
self.storagehelper.reset_storage(self.schema.value)
|
||||
self.init_storage()
|
||||
|
||||
@abstractmethod
|
||||
def check(self) -> bool:
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import io
|
||||
import secrets
|
||||
import threading
|
||||
import time
|
||||
@@ -24,6 +25,10 @@ class NoCheckInException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SessionInvalidException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AliPan(StorageBase, metaclass=Singleton):
|
||||
"""
|
||||
阿里云盘相关操作
|
||||
@@ -177,7 +182,7 @@ class AliPan(StorageBase, metaclass=Singleton):
|
||||
确认登录后,获取相关token
|
||||
"""
|
||||
if not self._auth_state:
|
||||
raise Exception("【阿里云盘】请先生成二维码")
|
||||
raise SessionInvalidException("【阿里云盘】请先生成二维码")
|
||||
resp = self.session.post(
|
||||
f"{self.base_url}/oauth/access_token",
|
||||
json={
|
||||
@@ -188,7 +193,7 @@ class AliPan(StorageBase, metaclass=Singleton):
|
||||
}
|
||||
)
|
||||
if resp is None:
|
||||
raise Exception("【阿里云盘】获取 access_token 失败")
|
||||
raise SessionInvalidException("【阿里云盘】获取 access_token 失败")
|
||||
result = resp.json()
|
||||
if result.get("code"):
|
||||
raise Exception(f"【阿里云盘】{result.get('code')} - {result.get('message')}!")
|
||||
@@ -199,7 +204,7 @@ class AliPan(StorageBase, metaclass=Singleton):
|
||||
刷新access_token
|
||||
"""
|
||||
if not refresh_token:
|
||||
raise Exception("【阿里云盘】会话失效,请重新扫码登录!")
|
||||
raise SessionInvalidException("【阿里云盘】会话失效,请重新扫码登录!")
|
||||
resp = self.session.post(
|
||||
f"{self.base_url}/oauth/access_token",
|
||||
json={
|
||||
@@ -335,6 +340,8 @@ class AliPan(StorageBase, metaclass=Singleton):
|
||||
"""
|
||||
if not fileinfo:
|
||||
return schemas.FileItem()
|
||||
if not parent.endswith("/"):
|
||||
parent += "/"
|
||||
if fileinfo.get("type") == "folder":
|
||||
return schemas.FileItem(
|
||||
storage=self.schema.value,
|
||||
@@ -437,7 +444,7 @@ class AliPan(StorageBase, metaclass=Singleton):
|
||||
"/adrive/v1.0/openFile/create",
|
||||
json={
|
||||
"drive_id": parent_item.drive_id,
|
||||
"parent_file_id": parent_item.fileid,
|
||||
"parent_file_id": parent_item.fileid or "root",
|
||||
"name": name,
|
||||
"type": "folder"
|
||||
}
|
||||
@@ -628,6 +635,29 @@ class AliPan(StorageBase, metaclass=Singleton):
|
||||
raise Exception(resp.get("message"))
|
||||
return resp
|
||||
|
||||
@staticmethod
|
||||
def _log_progress(desc: str, total: int) -> tqdm:
|
||||
"""
|
||||
创建一个可以输出到日志的进度条
|
||||
"""
|
||||
|
||||
class TqdmToLogger(io.StringIO):
|
||||
def write(s, buf): # noqa
|
||||
buf = buf.strip('\r\n\t ')
|
||||
if buf:
|
||||
logger.info(buf)
|
||||
|
||||
return tqdm(
|
||||
total=total,
|
||||
unit='B',
|
||||
unit_scale=True,
|
||||
desc=desc,
|
||||
file=TqdmToLogger(),
|
||||
mininterval=1.0,
|
||||
maxinterval=5.0,
|
||||
miniters=1
|
||||
)
|
||||
|
||||
def upload(self, target_dir: schemas.FileItem, local_path: Path,
|
||||
new_name: Optional[str] = None) -> Optional[schemas.FileItem]:
|
||||
"""
|
||||
@@ -668,13 +698,7 @@ class AliPan(StorageBase, metaclass=Singleton):
|
||||
|
||||
# 4. 初始化进度条
|
||||
logger.info(f"【阿里云盘】开始上传: {local_path} -> {target_path},分片数:{len(part_info_list)}")
|
||||
progress_bar = tqdm(
|
||||
total=file_size,
|
||||
unit='B',
|
||||
unit_scale=True,
|
||||
desc="上传进度",
|
||||
ascii=True
|
||||
)
|
||||
progress_bar = self._log_progress(f"【阿里云盘】{target_name} 上传进度", file_size)
|
||||
|
||||
# 5. 分片上传循环
|
||||
with open(local_path, 'rb') as f:
|
||||
@@ -828,7 +852,7 @@ class AliPan(StorageBase, metaclass=Singleton):
|
||||
if resp.get("code"):
|
||||
logger.debug(f"【阿里云盘】获取文件信息失败: {resp.get('message')}")
|
||||
return None
|
||||
return self.__get_fileitem(resp, parent=f"{str(path.parent)}/")
|
||||
return self.__get_fileitem(resp, parent=str(path.parent))
|
||||
except Exception as e:
|
||||
logger.debug(f"【阿里云盘】获取文件信息失败: {str(e)}")
|
||||
return None
|
||||
@@ -854,7 +878,7 @@ class AliPan(StorageBase, metaclass=Singleton):
|
||||
if folder:
|
||||
return folder
|
||||
# 逐级查找和创建目录
|
||||
fileitem = schemas.FileItem(storage=self.schema.value, path="/")
|
||||
fileitem = schemas.FileItem(storage=self.schema.value, path="/", drive_id=self._default_drive_id)
|
||||
for part in path.parts[1:]:
|
||||
dir_file = __find_dir(fileitem, part)
|
||||
if dir_file:
|
||||
@@ -957,3 +981,5 @@ class AliPan(StorageBase, metaclass=Singleton):
|
||||
)
|
||||
except NoCheckInException:
|
||||
return None
|
||||
except SessionInvalidException:
|
||||
return None
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import io
|
||||
import secrets
|
||||
import threading
|
||||
import time
|
||||
@@ -375,7 +375,7 @@ class U115Pan(StorageBase, metaclass=Singleton):
|
||||
"POST",
|
||||
"/open/folder/add",
|
||||
data={
|
||||
"pid": int(parent_item.fileid),
|
||||
"pid": int(parent_item.fileid or "0"),
|
||||
"file_name": name
|
||||
}
|
||||
)
|
||||
@@ -399,17 +399,37 @@ class U115Pan(StorageBase, metaclass=Singleton):
|
||||
modify_time=int(time.time())
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _log_progress(desc: str, total: int) -> tqdm:
|
||||
"""
|
||||
创建一个可以输出到日志的进度条
|
||||
"""
|
||||
|
||||
class TqdmToLogger(io.StringIO):
|
||||
def write(s, buf): # noqa
|
||||
buf = buf.strip('\r\n\t ')
|
||||
if buf:
|
||||
logger.info(buf)
|
||||
|
||||
return tqdm(
|
||||
total=total,
|
||||
unit='B',
|
||||
unit_scale=True,
|
||||
desc=desc,
|
||||
file=TqdmToLogger(),
|
||||
mininterval=1.0,
|
||||
maxinterval=5.0,
|
||||
miniters=1
|
||||
)
|
||||
|
||||
def upload(self, target_dir: schemas.FileItem, local_path: Path,
|
||||
new_name: Optional[str] = None) -> Optional[schemas.FileItem]:
|
||||
"""
|
||||
实现带秒传、断点续传和二次认证的文件上传
|
||||
"""
|
||||
|
||||
def encode_callback(cb: dict):
|
||||
"""
|
||||
回调参数Base64编码函数
|
||||
"""
|
||||
return oss2.utils.b64encode_as_string(json.dumps(cb).strip())
|
||||
def encode_callback(cb: str) -> str:
|
||||
return oss2.utils.b64encode_as_string(cb)
|
||||
|
||||
target_name = new_name or local_path.name
|
||||
target_path = Path(target_dir.path) / target_name
|
||||
@@ -535,12 +555,6 @@ class U115Pan(StorageBase, metaclass=Singleton):
|
||||
security_token=SecurityToken
|
||||
)
|
||||
bucket = oss2.Bucket(auth, endpoint, bucket_name) # noqa
|
||||
# 处理oss请求回调
|
||||
callback_dict = json.loads(callback.get("callback"))
|
||||
callback_var_dict = json.loads(callback.get("callback_var"))
|
||||
# 补充参数
|
||||
logger.debug(f"【115】上传 Step 6 回调参数:{callback_dict} {callback_var_dict}")
|
||||
# 填写不能包含Bucket名称在内的Object完整路径,例如exampledir/exampleobject.txt。
|
||||
# determine_part_size方法用于确定分片大小,设置分片大小为 100M
|
||||
part_size = determine_part_size(file_size, preferred_size=100 * 1024 * 1024)
|
||||
|
||||
@@ -584,8 +598,8 @@ class U115Pan(StorageBase, metaclass=Singleton):
|
||||
|
||||
# 请求头
|
||||
headers = {
|
||||
'X-oss-callback': encode_callback(callback_dict),
|
||||
'x-oss-callback-var': encode_callback(callback_var_dict),
|
||||
'X-oss-callback': encode_callback(callback["callback"]),
|
||||
'x-oss-callback-var': encode_callback(callback["callback_var"]),
|
||||
'x-oss-forbid-overwrite': 'false'
|
||||
}
|
||||
try:
|
||||
|
||||
@@ -56,7 +56,11 @@ class TYemaSiteUserInfo(SiteParserBase):
|
||||
self.join_at = StringUtils.unify_datetime_str(user_info.get("registerTime"))
|
||||
|
||||
self.upload = user_info.get('uploadSize')
|
||||
self.download = user_info.get('downloadSize')
|
||||
# 使用 promotionDownloadSize 获取真实下载量(考虑促销因素)
|
||||
if "promotionDownloadSize" in user_info:
|
||||
self.download = user_info.get('promotionDownloadSize')
|
||||
else:
|
||||
self.download = user_info.get('downloadSize')
|
||||
self.ratio = round(self.upload / (self.download or 1), 2)
|
||||
self.bonus = user_info.get("bonus")
|
||||
self.message_unread = 0
|
||||
|
||||
@@ -108,11 +108,17 @@ class MTorrentSpider:
|
||||
category = MediaType.MOVIE.value
|
||||
else:
|
||||
category = MediaType.UNKNOWN.value
|
||||
labels_value = self._labels.get(result.get('labels') or "0") or ""
|
||||
if labels_value:
|
||||
labels = labels_value.split()
|
||||
# 处理馒头新版标签
|
||||
labels = []
|
||||
labels_new = result.get( 'labelsNew' )
|
||||
if labels_new:
|
||||
# 新版标签本身就是list
|
||||
labels = labels_new
|
||||
else:
|
||||
labels = []
|
||||
# 旧版标签
|
||||
labels_value = self._labels.get(result.get('labels') or "0") or ""
|
||||
if labels_value:
|
||||
labels = labels_value.split()
|
||||
torrent = {
|
||||
'title': result.get('name'),
|
||||
'description': result.get('smallDescr'),
|
||||
|
||||
@@ -37,7 +37,7 @@ class TheMovieDbModule(_ModuleBase):
|
||||
self.cache = TmdbCache()
|
||||
self.tmdb = TmdbApi()
|
||||
self.category = CategoryHelper()
|
||||
self.scraper = TmdbScraper(self.tmdb)
|
||||
self.scraper = TmdbScraper()
|
||||
|
||||
@staticmethod
|
||||
def get_name() -> str:
|
||||
|
||||
@@ -7,15 +7,29 @@ from app.core.context import MediaInfo
|
||||
from app.core.meta import MetaBase
|
||||
from app.schemas.types import MediaType
|
||||
from app.utils.dom import DomUtils
|
||||
from app.modules.themoviedb.tmdbapi import TmdbApi
|
||||
|
||||
|
||||
class TmdbScraper:
|
||||
tmdb = None
|
||||
_force_nfo = False
|
||||
_force_img = False
|
||||
_meta_tmdb = None
|
||||
_img_tmdb = None
|
||||
|
||||
def __init__(self, tmdb):
|
||||
self.tmdb = tmdb
|
||||
@property
|
||||
def default_tmdb(self):
|
||||
"""
|
||||
获取元数据TMDB Api
|
||||
"""
|
||||
if not self._meta_tmdb:
|
||||
self._meta_tmdb = TmdbApi(language=settings.TMDB_LOCALE)
|
||||
return self._meta_tmdb
|
||||
|
||||
def original_tmdb(self, mediainfo: Optional[MediaInfo] = None):
|
||||
"""
|
||||
获取图片TMDB Api
|
||||
"""
|
||||
if settings.TMDB_SCRAP_ORIGINAL_IMAGE and mediainfo:
|
||||
return TmdbApi(language=mediainfo.original_language)
|
||||
return self.default_tmdb
|
||||
|
||||
def get_metadata_nfo(self, meta: MetaBase, mediainfo: MediaInfo,
|
||||
season: Optional[int] = None, episode: Optional[int] = None) -> Optional[str]:
|
||||
@@ -33,9 +47,9 @@ class TmdbScraper:
|
||||
if season is not None:
|
||||
# 查询季信息
|
||||
if mediainfo.episode_group:
|
||||
seasoninfo = self.tmdb.get_tv_group_detail(mediainfo.episode_group, season=season)
|
||||
seasoninfo = self.default_tmdb.get_tv_group_detail(mediainfo.episode_group, season=season)
|
||||
else:
|
||||
seasoninfo = self.tmdb.get_tv_season_detail(mediainfo.tmdb_id, season=season)
|
||||
seasoninfo = self.default_tmdb.get_tv_season_detail(mediainfo.tmdb_id, season=season)
|
||||
if episode:
|
||||
# 集元数据文件
|
||||
episodeinfo = self.__get_episode_detail(seasoninfo, meta.begin_episode)
|
||||
@@ -48,11 +62,12 @@ class TmdbScraper:
|
||||
# 电视剧元数据文件
|
||||
doc = self.__gen_tv_nfo_file(mediainfo=mediainfo)
|
||||
if doc:
|
||||
return doc.toprettyxml(indent=" ", encoding="utf-8") # noqa
|
||||
return doc.toprettyxml(indent=" ", encoding="utf-8") # noqa
|
||||
|
||||
return None
|
||||
|
||||
def get_metadata_img(self, mediainfo: MediaInfo, season: Optional[int] = None, episode: Optional[int] = None) -> dict:
|
||||
def get_metadata_img(self, mediainfo: MediaInfo, season: Optional[int] = None,
|
||||
episode: Optional[int] = None) -> dict:
|
||||
"""
|
||||
获取图片名称和url
|
||||
:param mediainfo: 媒体信息
|
||||
@@ -61,13 +76,13 @@ class TmdbScraper:
|
||||
"""
|
||||
images = {}
|
||||
if season is not None:
|
||||
# 只需要集的图片
|
||||
# 只需要季集的图片
|
||||
if episode:
|
||||
# 集的图片
|
||||
if mediainfo.episode_group:
|
||||
seasoninfo = self.tmdb.get_tv_group_detail(mediainfo.episode_group, season)
|
||||
seasoninfo = self.original_tmdb(mediainfo).get_tv_group_detail(mediainfo.episode_group, season)
|
||||
else:
|
||||
seasoninfo = self.tmdb.get_tv_season_detail(mediainfo.tmdb_id, season)
|
||||
seasoninfo = self.original_tmdb(mediainfo).get_tv_season_detail(mediainfo.tmdb_id, season)
|
||||
if seasoninfo:
|
||||
episodeinfo = self.__get_episode_detail(seasoninfo, episode)
|
||||
if episodeinfo and episodeinfo.get("still_path"):
|
||||
@@ -77,7 +92,7 @@ class TmdbScraper:
|
||||
images[still_name] = still_url
|
||||
else:
|
||||
# 季的图片
|
||||
seasoninfo = self.tmdb.get_tv_season_detail(mediainfo.tmdb_id, season)
|
||||
seasoninfo = self.original_tmdb(mediainfo).get_tv_season_detail(mediainfo.tmdb_id, season)
|
||||
if seasoninfo:
|
||||
# TMDB季poster图片
|
||||
poster_name, poster_url = self.get_season_poster(seasoninfo, season)
|
||||
@@ -85,7 +100,7 @@ class TmdbScraper:
|
||||
images[poster_name] = poster_url
|
||||
return images
|
||||
else:
|
||||
# 主媒体图片
|
||||
# 获取媒体信息中原有图片(TheMovieDb或Fanart)
|
||||
for attr_name, attr_value in vars(mediainfo).items():
|
||||
if attr_value \
|
||||
and attr_name.endswith("_path") \
|
||||
@@ -94,6 +109,15 @@ class TmdbScraper:
|
||||
and attr_value.startswith("http"):
|
||||
image_name = attr_name.replace("_path", "") + Path(attr_value).suffix
|
||||
images[image_name] = attr_value
|
||||
# 替换原语言Poster
|
||||
if settings.TMDB_SCRAP_ORIGINAL_IMAGE:
|
||||
_mediainfo = self.original_tmdb(mediainfo).get_info(mediainfo.type, mediainfo.tmdb_id)
|
||||
if _mediainfo:
|
||||
for attr_name, attr_value in _mediainfo.items():
|
||||
if attr_name.endswith("_path") and attr_value is not None:
|
||||
image_url = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{attr_value}"
|
||||
image_name = attr_name.replace("_path", "") + Path(image_url).suffix
|
||||
images[image_name] = image_url
|
||||
return images
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -23,31 +23,19 @@ class TmdbApi:
|
||||
TMDB识别匹配
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, language: Optional[str] = None):
|
||||
# TMDB主体
|
||||
self.tmdb = TMDb()
|
||||
# 域名
|
||||
self.tmdb.domain = settings.TMDB_API_DOMAIN
|
||||
# 开启缓存
|
||||
self.tmdb.cache = True
|
||||
# APIKEY
|
||||
self.tmdb.api_key = settings.TMDB_API_KEY
|
||||
# 语种
|
||||
self.tmdb.language = settings.TMDB_LOCALE
|
||||
# 代理
|
||||
self.tmdb.proxies = settings.PROXY
|
||||
# 调试模式
|
||||
self.tmdb.debug = False
|
||||
self.tmdb = TMDb(language=language)
|
||||
# TMDB查询对象
|
||||
self.search = Search()
|
||||
self.movie = Movie()
|
||||
self.tv = TV()
|
||||
self.season_obj = Season()
|
||||
self.episode_obj = Episode()
|
||||
self.discover = Discover()
|
||||
self.trending = Trending()
|
||||
self.person = Person()
|
||||
self.collection = Collection()
|
||||
self.search = Search(language=language)
|
||||
self.movie = Movie(language=language)
|
||||
self.tv = TV(language=language)
|
||||
self.season_obj = Season(language=language)
|
||||
self.episode_obj = Episode(language=language)
|
||||
self.discover = Discover(language=language)
|
||||
self.trending = Trending(language=language)
|
||||
self.person = Person(language=language)
|
||||
self.collection = Collection(language=language)
|
||||
|
||||
def search_multiis(self, title: str) -> List[dict]:
|
||||
"""
|
||||
@@ -648,6 +636,7 @@ class TmdbApi:
|
||||
return None
|
||||
# dict[地区:分级]
|
||||
ratings = {}
|
||||
results = []
|
||||
if results := (tmdb_info.get("release_dates") or {}).get("results"):
|
||||
"""
|
||||
[
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
@@ -17,19 +16,22 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TMDb(object):
|
||||
TMDB_API_KEY = "TMDB_API_KEY"
|
||||
TMDB_LANGUAGE = "TMDB_LANGUAGE"
|
||||
TMDB_SESSION_ID = "TMDB_SESSION_ID"
|
||||
TMDB_WAIT_ON_RATE_LIMIT = "TMDB_WAIT_ON_RATE_LIMIT"
|
||||
TMDB_DEBUG_ENABLED = "TMDB_DEBUG_ENABLED"
|
||||
TMDB_CACHE_ENABLED = "TMDB_CACHE_ENABLED"
|
||||
TMDB_PROXIES = "TMDB_PROXIES"
|
||||
TMDB_DOMAIN = "TMDB_DOMAIN"
|
||||
|
||||
_req = None
|
||||
_session = None
|
||||
|
||||
def __init__(self, obj_cached=True, session=None):
|
||||
def __init__(self, obj_cached=True, session=None, language=None):
|
||||
self._api_key = settings.TMDB_API_KEY
|
||||
self._language = language or settings.TMDB_LOCALE or "en-US"
|
||||
self._session_id = None
|
||||
self._wait_on_rate_limit = True
|
||||
self._debug_enabled = False
|
||||
self._cache_enabled = obj_cached
|
||||
self._proxies = settings.PROXY
|
||||
self._domain = settings.TMDB_API_DOMAIN
|
||||
self._page = None
|
||||
self._total_results = None
|
||||
self._total_pages = None
|
||||
|
||||
if session is not None:
|
||||
self._req = RequestUtils(session=session, proxies=self.proxies)
|
||||
else:
|
||||
@@ -39,103 +41,88 @@ class TMDb(object):
|
||||
self._reset = None
|
||||
self._timeout = 15
|
||||
self.obj_cached = obj_cached
|
||||
if os.environ.get(self.TMDB_LANGUAGE) is None:
|
||||
os.environ[self.TMDB_LANGUAGE] = "en-US"
|
||||
|
||||
@property
|
||||
def page(self):
|
||||
return os.environ["page"]
|
||||
return self._page
|
||||
|
||||
@property
|
||||
def total_results(self):
|
||||
return os.environ["total_results"]
|
||||
return self._total_results
|
||||
|
||||
@property
|
||||
def total_pages(self):
|
||||
return os.environ["total_pages"]
|
||||
return self._total_pages
|
||||
|
||||
@property
|
||||
def api_key(self):
|
||||
return os.environ.get(self.TMDB_API_KEY)
|
||||
return self._api_key
|
||||
|
||||
@property
|
||||
def domain(self):
|
||||
return os.environ.get(self.TMDB_DOMAIN)
|
||||
return self._domain
|
||||
|
||||
@property
|
||||
def proxies(self):
|
||||
proxy = os.environ.get(self.TMDB_PROXIES)
|
||||
if proxy is not None:
|
||||
proxy = eval(proxy)
|
||||
return proxy
|
||||
return self._proxies
|
||||
|
||||
@proxies.setter
|
||||
def proxies(self, proxies):
|
||||
if proxies is not None:
|
||||
os.environ[self.TMDB_PROXIES] = str(proxies)
|
||||
self._proxies = proxies
|
||||
|
||||
@api_key.setter
|
||||
def api_key(self, api_key):
|
||||
os.environ[self.TMDB_API_KEY] = str(api_key)
|
||||
self._api_key = str(api_key)
|
||||
|
||||
@domain.setter
|
||||
def domain(self, domain):
|
||||
os.environ[self.TMDB_DOMAIN] = str(domain)
|
||||
self._domain = str(domain)
|
||||
|
||||
@property
|
||||
def language(self):
|
||||
return os.environ.get(self.TMDB_LANGUAGE)
|
||||
return self._language
|
||||
|
||||
@language.setter
|
||||
def language(self, language):
|
||||
os.environ[self.TMDB_LANGUAGE] = language
|
||||
self._language = language
|
||||
|
||||
@property
|
||||
def has_session(self):
|
||||
return True if os.environ.get(self.TMDB_SESSION_ID) else False
|
||||
return True if self._session_id else False
|
||||
|
||||
@property
|
||||
def session_id(self):
|
||||
if not os.environ.get(self.TMDB_SESSION_ID):
|
||||
if not self._session_id:
|
||||
raise TMDbException("Must Authenticate to create a session run Authentication(username, password)")
|
||||
return os.environ.get(self.TMDB_SESSION_ID)
|
||||
return self._session_id
|
||||
|
||||
@session_id.setter
|
||||
def session_id(self, session_id):
|
||||
os.environ[self.TMDB_SESSION_ID] = session_id
|
||||
self._session_id = session_id
|
||||
|
||||
@property
|
||||
def wait_on_rate_limit(self):
|
||||
if os.environ.get(self.TMDB_WAIT_ON_RATE_LIMIT) == "False":
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return self._wait_on_rate_limit
|
||||
|
||||
@wait_on_rate_limit.setter
|
||||
def wait_on_rate_limit(self, wait_on_rate_limit):
|
||||
os.environ[self.TMDB_WAIT_ON_RATE_LIMIT] = str(wait_on_rate_limit)
|
||||
self._wait_on_rate_limit = bool(wait_on_rate_limit)
|
||||
|
||||
@property
|
||||
def debug(self):
|
||||
if os.environ.get(self.TMDB_DEBUG_ENABLED) == "True":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return self._debug_enabled
|
||||
|
||||
@debug.setter
|
||||
def debug(self, debug):
|
||||
os.environ[self.TMDB_DEBUG_ENABLED] = str(debug)
|
||||
self._debug_enabled = bool(debug)
|
||||
|
||||
@property
|
||||
def cache(self):
|
||||
if os.environ.get(self.TMDB_CACHE_ENABLED) == "False":
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return self._cache_enabled
|
||||
|
||||
@cache.setter
|
||||
def cache(self, cache):
|
||||
os.environ[self.TMDB_CACHE_ENABLED] = str(cache)
|
||||
self._cache_enabled = bool(cache)
|
||||
|
||||
@cached(maxsize=settings.CACHE_CONF["tmdb"], ttl=settings.CACHE_CONF["meta"])
|
||||
def cached_request(self, method, url, data, json,
|
||||
@@ -197,30 +184,30 @@ class TMDb(object):
|
||||
else:
|
||||
raise TMDbException("达到请求频率限制,将在 %d 秒后重试..." % sleep_time)
|
||||
|
||||
json = req.json()
|
||||
json_data = req.json()
|
||||
|
||||
if "page" in json:
|
||||
os.environ["page"] = str(json["page"])
|
||||
if "page" in json_data:
|
||||
self._page = json_data["page"]
|
||||
|
||||
if "total_results" in json:
|
||||
os.environ["total_results"] = str(json["total_results"])
|
||||
if "total_results" in json_data:
|
||||
self._total_results = json_data["total_results"]
|
||||
|
||||
if "total_pages" in json:
|
||||
os.environ["total_pages"] = str(json["total_pages"])
|
||||
if "total_pages" in json_data:
|
||||
self._total_pages = json_data["total_pages"]
|
||||
|
||||
if self.debug:
|
||||
logger.info(json)
|
||||
logger.info(json_data)
|
||||
logger.info(self.cached_request.cache_info())
|
||||
|
||||
if "errors" in json:
|
||||
raise TMDbException(json["errors"])
|
||||
if "errors" in json_data:
|
||||
raise TMDbException(json_data["errors"])
|
||||
|
||||
if "success" in json and json["success"] is False:
|
||||
raise TMDbException(json["status_message"])
|
||||
if "success" in json_data and json_data["success"] is False:
|
||||
raise TMDbException(json_data["status_message"])
|
||||
|
||||
if key:
|
||||
return json.get(key)
|
||||
return json
|
||||
return json_data.get(key)
|
||||
return json_data
|
||||
|
||||
def close(self):
|
||||
if self._session:
|
||||
|
||||
5
app/modules/transmission/transmission.py
Normal file → Executable file
5
app/modules/transmission/transmission.py
Normal file → Executable file
@@ -163,8 +163,9 @@ class Transmission:
|
||||
if not self.trc:
|
||||
return []
|
||||
try:
|
||||
torrent = self.trc.get_torrents(ids=ids, arguments=self._trarg)
|
||||
if torrent:
|
||||
torrents = self.trc.get_torrents(ids=ids, arguments=self._trarg)
|
||||
if len(torrents):
|
||||
torrent = torrents[0]
|
||||
labels = [str(tag).strip()
|
||||
for tag in torrent.labels] if hasattr(torrent, "labels") else []
|
||||
return labels
|
||||
|
||||
@@ -44,6 +44,7 @@ class TrimeMedia:
|
||||
self._playhost = play_api.host
|
||||
elif play_host:
|
||||
logger.warning(f"请检查外网播放地址 {play_host}")
|
||||
self._playhost = UrlUtils.standardize_base_url(play_host).rstrip("/")
|
||||
|
||||
self.reconnect()
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@ class _PluginBase(metaclass=ABCMeta):
|
||||
"path": "/xx",
|
||||
"endpoint": self.xxx,
|
||||
"methods": ["GET", "POST"],
|
||||
"auth: "apikey", # 鉴权类型:apikey/bear
|
||||
"summary": "API名称",
|
||||
"description": "API说明"
|
||||
}]
|
||||
|
||||
@@ -51,6 +51,8 @@ class Scheduler(metaclass=Singleton):
|
||||
_jobs = {}
|
||||
# 用户认证失败次数
|
||||
_auth_count = 0
|
||||
# 用户认证失败消息发送
|
||||
_auth_message = False
|
||||
|
||||
def __init__(self):
|
||||
self.init()
|
||||
@@ -586,6 +588,9 @@ class Scheduler(metaclass=Singleton):
|
||||
schedulers = []
|
||||
# 去重
|
||||
added = []
|
||||
# 避免_scheduler.shutdown()处于阻塞状态导致的死锁
|
||||
if not self._scheduler or not self._scheduler.running:
|
||||
return []
|
||||
jobs = self._scheduler.get_jobs()
|
||||
# 按照下次运行时间排序
|
||||
jobs.sort(key=lambda x: x.next_run_time)
|
||||
@@ -658,9 +663,11 @@ class Scheduler(metaclass=Singleton):
|
||||
# 最大重试次数
|
||||
__max_try__ = 30
|
||||
if self._auth_count > __max_try__:
|
||||
SchedulerChain().messagehelper.put(title=f"用户认证失败",
|
||||
message="用户认证失败次数过多,将不再尝试认证!",
|
||||
role="system")
|
||||
if not self._auth_message:
|
||||
SchedulerChain().messagehelper.put(title=f"用户认证失败",
|
||||
message="用户认证失败次数过多,将不再尝试认证!",
|
||||
role="system")
|
||||
self._auth_message = True
|
||||
return
|
||||
logger.info("用户未认证,正在尝试认证...")
|
||||
auth_conf = SystemConfigOper().get(SystemConfigKey.UserSiteAuthParams)
|
||||
@@ -675,10 +682,11 @@ class Scheduler(metaclass=Singleton):
|
||||
Notification(
|
||||
mtype=NotificationType.Manual,
|
||||
title="MoviePilot用户认证成功",
|
||||
text=f"使用站点:{msg}",
|
||||
text=f"使用站点:{msg},如有插件使用异常,请重启MoviePilot。",
|
||||
link=settings.MP_DOMAIN('#/site')
|
||||
)
|
||||
)
|
||||
# 认证通过后重新初始化插件
|
||||
PluginManager().init_config()
|
||||
self.init_plugin_jobs()
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Optional, Union
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
@@ -197,13 +197,13 @@ class ContentType(str, Enum):
|
||||
操作状态的通知消息类型标识
|
||||
"""
|
||||
# 订阅添加成功
|
||||
SubscribeAdded: str = "subscribeAdded"
|
||||
SubscribeAdded = "subscribeAdded"
|
||||
# 订阅完成
|
||||
SubscribeComplete: str = "subscribeComplete"
|
||||
SubscribeComplete = "subscribeComplete"
|
||||
# 入库成功
|
||||
OrganizeSuccess: str = "organizeSuccess"
|
||||
OrganizeSuccess = "organizeSuccess"
|
||||
# 下载开始(添加下载任务成功)
|
||||
DownloadAdded: str = "downloadAdded"
|
||||
DownloadAdded = "downloadAdded"
|
||||
|
||||
|
||||
# 消息渠道
|
||||
|
||||
22
app/startup/command_initializer.py
Normal file
22
app/startup/command_initializer.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from app.command import Command
|
||||
|
||||
|
||||
def init_command():
|
||||
"""
|
||||
初始化命令
|
||||
"""
|
||||
Command()
|
||||
|
||||
|
||||
def stop_command():
|
||||
"""
|
||||
停止命令
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def restart_command():
|
||||
"""
|
||||
重启命令
|
||||
"""
|
||||
Command().init_commands()
|
||||
@@ -3,10 +3,25 @@ from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
from app.startup.workflow_initializer import init_workflow, stop_workflow
|
||||
from app.startup.modules_initializer import shutdown_modules, start_modules
|
||||
from app.startup.plugins_initializer import init_plugins_async
|
||||
from app.core.config import global_vars
|
||||
from app.startup.command_initializer import init_command, stop_command, restart_command
|
||||
from app.startup.modules_initializer import init_modules, stop_modules
|
||||
from app.startup.monitor_initializer import stop_monitor, init_monitor
|
||||
from app.startup.plugins_initializer import init_plugins, stop_plugins, sync_plugins
|
||||
from app.startup.routers_initializer import init_routers
|
||||
from app.startup.scheduler_initializer import stop_scheduler, init_scheduler, init_plugin_scheduler
|
||||
from app.startup.workflow_initializer import init_workflow, stop_workflow
|
||||
|
||||
|
||||
async def init_plugin_system():
|
||||
"""
|
||||
同步插件及重启相关依赖服务
|
||||
"""
|
||||
if await sync_plugins():
|
||||
# 重新注册插件定时服务
|
||||
init_plugin_scheduler()
|
||||
# 重新注册命令
|
||||
restart_command()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -15,29 +30,45 @@ async def lifespan(app: FastAPI):
|
||||
定义应用的生命周期事件
|
||||
"""
|
||||
print("Starting up...")
|
||||
# 启动模块
|
||||
start_modules(app)
|
||||
# 初始化工作流动作
|
||||
init_workflow(app)
|
||||
# 初始化模块
|
||||
init_modules()
|
||||
# 初始化路由
|
||||
init_routers(app)
|
||||
# 初始化插件
|
||||
plugin_init_task = asyncio.create_task(init_plugins_async())
|
||||
init_plugins()
|
||||
# 初始化定时器
|
||||
init_scheduler()
|
||||
# 初始化监控器
|
||||
init_monitor()
|
||||
# 初始化命令
|
||||
init_command()
|
||||
# 初始化工作流
|
||||
init_workflow()
|
||||
# 插件同步到本地
|
||||
sync_plugins_task = asyncio.create_task(init_plugin_system())
|
||||
try:
|
||||
# 在此处 yield,表示应用已经启动,控制权交回 FastAPI 主事件循环
|
||||
yield
|
||||
finally:
|
||||
print("Shutting down...")
|
||||
# 停止信号
|
||||
global_vars.stop_system()
|
||||
try:
|
||||
# 取消插件初始化
|
||||
plugin_init_task.cancel()
|
||||
await plugin_init_task
|
||||
sync_plugins_task.cancel()
|
||||
await sync_plugins_task
|
||||
except asyncio.CancelledError:
|
||||
print("Plugin installation task cancelled.")
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"Error during plugin installation shutdown: {e}")
|
||||
# 清理模块
|
||||
shutdown_modules(app)
|
||||
# 关闭工作流
|
||||
stop_workflow(app)
|
||||
|
||||
print(str(e))
|
||||
# 停止工作流
|
||||
stop_workflow()
|
||||
# 停止命令
|
||||
stop_command()
|
||||
# 停止监控器
|
||||
stop_monitor()
|
||||
# 停止定时器
|
||||
stop_scheduler()
|
||||
# 停止插件
|
||||
stop_plugins()
|
||||
# 停止模块
|
||||
stop_modules()
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import sys
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
from app.core.cache import close_cache
|
||||
from app.core.config import global_vars, settings
|
||||
from app.core.config import settings
|
||||
from app.core.module import ModuleManager
|
||||
from app.log import logger
|
||||
from app.utils.system import SystemUtils
|
||||
from app.command import CommandChain
|
||||
|
||||
# SitesHelper涉及资源包拉取,提前引入并容错提示
|
||||
try:
|
||||
@@ -18,18 +17,14 @@ except ImportError as e:
|
||||
sys.exit(1)
|
||||
|
||||
from app.core.event import EventManager
|
||||
from app.core.plugin import PluginManager
|
||||
from app.helper.thread import ThreadHelper
|
||||
from app.helper.display import DisplayHelper
|
||||
from app.helper.resource import ResourceHelper
|
||||
from app.helper.message import MessageHelper
|
||||
from app.scheduler import Scheduler
|
||||
from app.monitor import Monitor
|
||||
from app.schemas import Notification, NotificationType
|
||||
from app.schemas.types import SystemConfigKey
|
||||
from app.db import close_database
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.command import Command, CommandChain
|
||||
|
||||
|
||||
def start_frontend():
|
||||
@@ -109,25 +104,16 @@ def check_auth():
|
||||
)
|
||||
|
||||
|
||||
def shutdown_modules(_: FastAPI):
|
||||
def stop_modules():
|
||||
"""
|
||||
服务关闭
|
||||
"""
|
||||
# 停止信号
|
||||
global_vars.stop_system()
|
||||
# 停止模块
|
||||
ModuleManager().stop()
|
||||
# 停止插件
|
||||
PluginManager().stop()
|
||||
PluginManager().stop_monitor()
|
||||
# 停止事件消费
|
||||
EventManager().stop()
|
||||
# 停止虚拟显示
|
||||
DisplayHelper().stop()
|
||||
# 停止定时服务
|
||||
Scheduler().stop()
|
||||
# 停止监控
|
||||
Monitor().stop()
|
||||
# 停止线程池
|
||||
ThreadHelper().shutdown()
|
||||
# 停止缓存连接
|
||||
@@ -140,7 +126,7 @@ def shutdown_modules(_: FastAPI):
|
||||
clear_temp()
|
||||
|
||||
|
||||
def start_modules(_: FastAPI):
|
||||
def init_modules():
|
||||
"""
|
||||
启动模块
|
||||
"""
|
||||
@@ -156,14 +142,6 @@ def start_modules(_: FastAPI):
|
||||
ModuleManager()
|
||||
# 启动事件消费
|
||||
EventManager().start()
|
||||
# 加载插件
|
||||
PluginManager().start()
|
||||
# 启动监控任务
|
||||
Monitor()
|
||||
# 启动定时服务
|
||||
Scheduler()
|
||||
# 加载命令
|
||||
Command()
|
||||
# 启动前端服务
|
||||
start_frontend()
|
||||
# 检查认证状态
|
||||
|
||||
15
app/startup/monitor_initializer.py
Normal file
15
app/startup/monitor_initializer.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from app.monitor import Monitor
|
||||
|
||||
|
||||
def init_monitor():
|
||||
"""
|
||||
初始化监控器
|
||||
"""
|
||||
Monitor()
|
||||
|
||||
|
||||
def stop_monitor():
|
||||
"""
|
||||
停止监控器
|
||||
"""
|
||||
Monitor().stop()
|
||||
@@ -1,43 +1,36 @@
|
||||
import asyncio
|
||||
|
||||
from app.command import Command
|
||||
from app.core.plugin import PluginManager
|
||||
from app.log import logger
|
||||
from app.scheduler import Scheduler
|
||||
|
||||
|
||||
async def init_plugins_async():
|
||||
async def sync_plugins() -> bool:
|
||||
"""
|
||||
初始化安装插件,并动态注册后台任务及API
|
||||
"""
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
plugin_manager = PluginManager()
|
||||
scheduler = Scheduler()
|
||||
command = Command()
|
||||
|
||||
sync_result = await execute_task(loop, plugin_manager.sync, "插件同步到本地")
|
||||
resolved_dependencies = await execute_task(loop, plugin_manager.install_plugin_missing_dependencies,
|
||||
"缺失依赖项安装")
|
||||
# 判断是否需要进行插件初始化
|
||||
if not sync_result and not resolved_dependencies:
|
||||
logger.debug("没有新的插件同步到本地或缺失依赖项需要安装,跳过插件初始化")
|
||||
return
|
||||
logger.debug("没有新的插件同步到本地或缺失依赖项需要安装")
|
||||
return False
|
||||
|
||||
# 继续执行后续的插件初始化步骤
|
||||
logger.info("正在初始化所有插件")
|
||||
# 为避免初始化插件异常,这里所有插件都进行初始化
|
||||
# 安装完成后重新初始化插件
|
||||
logger.info("正在重新初始化插件")
|
||||
# 重新初始化插件
|
||||
plugin_manager.init_config()
|
||||
# 插件启动后注册后台任务
|
||||
scheduler.init_plugin_jobs()
|
||||
# 插件启动后注册菜单命令
|
||||
command.init_commands()
|
||||
# 插件启动后注册插件API
|
||||
# 重新注册插件API
|
||||
register_plugin_api()
|
||||
logger.info("所有插件初始化完成")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"插件初始化过程中出现异常: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def execute_task(loop, task_func, task_name):
|
||||
@@ -62,3 +55,23 @@ def register_plugin_api():
|
||||
"""
|
||||
from app.api.endpoints import plugin
|
||||
plugin.register_plugin_api()
|
||||
|
||||
|
||||
def init_plugins():
|
||||
"""
|
||||
初始化插件
|
||||
"""
|
||||
PluginManager().start()
|
||||
register_plugin_api()
|
||||
|
||||
|
||||
def stop_plugins():
|
||||
"""
|
||||
停止插件
|
||||
"""
|
||||
try:
|
||||
plugin_manager = PluginManager()
|
||||
plugin_manager.stop()
|
||||
plugin_manager.stop_monitor()
|
||||
except Exception as e:
|
||||
logger.error(f"停止插件时发生错误:{e}", exc_info=True)
|
||||
|
||||
29
app/startup/scheduler_initializer.py
Normal file
29
app/startup/scheduler_initializer.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from app.scheduler import Scheduler
|
||||
|
||||
|
||||
def init_scheduler():
|
||||
"""
|
||||
初始化定时器
|
||||
"""
|
||||
Scheduler()
|
||||
|
||||
|
||||
def stop_scheduler():
|
||||
"""
|
||||
停止定时器
|
||||
"""
|
||||
Scheduler().stop()
|
||||
|
||||
|
||||
def restart_scheduler():
|
||||
"""
|
||||
重启定时器
|
||||
"""
|
||||
Scheduler().init()
|
||||
|
||||
|
||||
def init_plugin_scheduler():
|
||||
"""
|
||||
初始化插件定时器
|
||||
"""
|
||||
Scheduler().init_plugin_jobs()
|
||||
@@ -1,16 +1,14 @@
|
||||
from fastapi import FastAPI
|
||||
|
||||
from app.core.workflow import WorkFlowManager
|
||||
|
||||
|
||||
def init_workflow(_: FastAPI):
|
||||
def init_workflow():
|
||||
"""
|
||||
初始化动作
|
||||
"""
|
||||
WorkFlowManager()
|
||||
|
||||
|
||||
def stop_workflow(_: FastAPI):
|
||||
def stop_workflow():
|
||||
"""
|
||||
停止动作
|
||||
"""
|
||||
|
||||
@@ -52,21 +52,30 @@ class ObjectUtils:
|
||||
# 跳过空行
|
||||
if not line:
|
||||
continue
|
||||
# 处理多行注释
|
||||
# 处理"""单行注释
|
||||
if (line.startswith(('"""', "'''"))
|
||||
and line.endswith(('"""', "'''"))
|
||||
and len(line) > 3):
|
||||
continue
|
||||
# 处理"""多行注释
|
||||
if line.startswith(('"""', "'''")):
|
||||
in_comment = not in_comment
|
||||
continue
|
||||
# 在注释中则跳过
|
||||
if in_comment:
|
||||
continue
|
||||
# 跳过注释、pass语句、装饰器、函数定义行
|
||||
if line.startswith('#') or line == "pass" or line.startswith('@') or line.startswith('def '):
|
||||
# 跳过#注释、pass语句、装饰器、函数定义行
|
||||
if (line.startswith('#')
|
||||
or line == "pass"
|
||||
or line.startswith('@')
|
||||
or line.startswith('def ')):
|
||||
continue
|
||||
# 发现有效代码行
|
||||
return True
|
||||
# 没有有效代码行
|
||||
return False
|
||||
except Exception:
|
||||
except Exception as err:
|
||||
print(err)
|
||||
# 源代码分析失败时,进行字节码分析
|
||||
code_obj = func.__code__
|
||||
instructions = list(dis.get_instructions(code_obj))
|
||||
|
||||
@@ -15,7 +15,8 @@ from app.schemas.types import MediaType
|
||||
_special_domains = [
|
||||
'u2.dmhy.org',
|
||||
'pt.ecust.pp.ua',
|
||||
'pt.gtkpw.xyz'
|
||||
'pt.gtkpw.xyz',
|
||||
'pt.gtk.pw'
|
||||
]
|
||||
|
||||
# 内置版本号转换字典
|
||||
|
||||
37
database/versions/486e56a62dcb_2_1_5.py
Normal file
37
database/versions/486e56a62dcb_2_1_5.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""2.1.5
|
||||
|
||||
Revision ID: 486e56a62dcb
|
||||
Revises: 89d24811e894
|
||||
Create Date: 2025-05-13 19:49:51.271319
|
||||
|
||||
"""
|
||||
import re
|
||||
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.schemas.types import SystemConfigKey
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '486e56a62dcb'
|
||||
down_revision = '89d24811e894'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
### 将消息模板中的 `season`(为单数字, 且重命名需要这个字段)替换为 `season_fmt`(Sxx格式字符串) ###
|
||||
_systemconfig = SystemConfigOper()
|
||||
templates = _systemconfig.get(SystemConfigKey.NotificationTemplates)
|
||||
if isinstance(templates, dict):
|
||||
_re = r'(?<={{)(?![^}]*[%|])(\s*)season(\s*)(?=}})|(?<={%)if\s+(?![^%]*[%|])season\s*(?=%)'
|
||||
for k, v in templates.items():
|
||||
# 替换season为season_fmt
|
||||
result = re.sub(_re, r'\1season_fmt\2', v)
|
||||
templates[k] = result
|
||||
# 将更新后的模板存回系统配置
|
||||
_systemconfig.set(SystemConfigKey.NotificationTemplates, templates)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@@ -33,7 +33,7 @@ def upgrade() -> None:
|
||||
"downloadAdded": """
|
||||
{
|
||||
'title': '{{ title_year }}'
|
||||
'{% if download_episodes %} {{ season }} {{ download_episodes }}{% else %}{{ season_episode }}{% endif %} 开始下载',
|
||||
'{% if download_episodes %} {{ season_fmt }} {{ download_episodes }}{% else %}{{ season_episode }}{% endif %} 开始下载',
|
||||
'text': '{% if site_name %}站点:{{ site_name }}{% endif %}'
|
||||
'{% if resource_term %}\\n质量:{{ resource_term }}{% endif %}'
|
||||
'{% if size %}\\n大小:{{ size }}{% endif %}'
|
||||
@@ -46,10 +46,11 @@ def upgrade() -> None:
|
||||
'{% if labels %}\\n标签:{{ labels }}{% endif %}'
|
||||
'{% if description %}\\n描述:{{ description }}{% endif %}'
|
||||
}""",
|
||||
"subscribeAdded": "{'title': '{{ title_year }} {{season}} 已添加订阅'}",
|
||||
"subscribeAdded": "{'title': '{{ title_year }}{% if season_fmt %} {{ season_fmt }}{% endif %} 已添加订阅'}",
|
||||
"subscribeComplete": """
|
||||
{
|
||||
'title': '{{ title_year }} {{season}} 已完成{{msgstr}}',
|
||||
'title': '{{ title_year }}'
|
||||
'{% if season_fmt %} {{ season_fmt }}{% endif %} 已完成{{ msgstr }}',
|
||||
'text': '{% if vote_average %}评分:{{ vote_average }}{% endif %}'
|
||||
'{% if username %},来自用户:{{ username }}{% endif %}'
|
||||
'{% if actors %}\\n演员:{{ actors }}{% endif %}'
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
APP_VERSION = 'v2.4.4'
|
||||
FRONTEND_VERSION = 'v2.4.4'
|
||||
APP_VERSION = 'v2.4.7'
|
||||
FRONTEND_VERSION = 'v2.4.7'
|
||||
|
||||
Reference in New Issue
Block a user