mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-14 23:16:48 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2552219991 | ||
|
|
a038b698d7 | ||
|
|
a3b222574e | ||
|
|
e0cd467293 | ||
|
|
9c056030d2 | ||
|
|
19efa9d4cc | ||
|
|
90633a6495 | ||
|
|
edc432fbd8 | ||
|
|
1b7bdbf516 |
@@ -541,6 +541,9 @@ class MediaChain(ChainBase):
|
||||
# 处理目录内的文件
|
||||
files = __list_files(_fileitem=fileitem)
|
||||
for file in files:
|
||||
if file.type == "dir":
|
||||
# 电影不处理子目录
|
||||
continue
|
||||
self.scrape_metadata(fileitem=file,
|
||||
mediainfo=mediainfo,
|
||||
init_folder=False,
|
||||
@@ -640,6 +643,9 @@ class MediaChain(ChainBase):
|
||||
if recursive:
|
||||
files = __list_files(_fileitem=fileitem)
|
||||
for file in files:
|
||||
if file.type == "dir" and not file.name.lower().startswith("season"):
|
||||
# 电视剧不处理非季子目录
|
||||
continue
|
||||
self.scrape_metadata(fileitem=file,
|
||||
mediainfo=mediainfo,
|
||||
parent=fileitem if file.type == "file" else None,
|
||||
|
||||
@@ -591,7 +591,7 @@ class TransferChain(ChainBase, metaclass=Singleton):
|
||||
text=__process_msg,
|
||||
data={
|
||||
"current": Path(fileitem.path).as_posix(),
|
||||
"finished":finished_files
|
||||
"finished": finished_files
|
||||
})
|
||||
# 整理
|
||||
state, err_msg = self.__handle_transfer(task=task, callback=item.callback)
|
||||
@@ -1471,13 +1471,9 @@ class TransferChain(ChainBase, metaclass=Singleton):
|
||||
for file in torrent_files:
|
||||
file_path = save_path / file.name
|
||||
# 如果存在未被屏蔽的媒体文件,则不删除种子
|
||||
if (
|
||||
file_path.suffix in self.all_exts
|
||||
and not self._is_blocked_by_exclude_words(
|
||||
str(file_path), transfer_exclude_words
|
||||
)
|
||||
and file_path.exists()
|
||||
):
|
||||
if (file_path.suffix in self.all_exts
|
||||
and not self._is_blocked_by_exclude_words(str(file_path), transfer_exclude_words)
|
||||
and file_path.exists()):
|
||||
return False
|
||||
|
||||
# 所有媒体文件都被屏蔽或不存在,可以删除种子
|
||||
|
||||
@@ -610,14 +610,19 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
asset = next((a for a in assets if a.get("name") == asset_name), None)
|
||||
if not asset:
|
||||
return False, f"未找到资产文件:{asset_name}"
|
||||
download_url = asset.get("browser_download_url")
|
||||
if not download_url:
|
||||
return False, "资产缺少下载地址"
|
||||
asset_id = asset.get("id")
|
||||
if not asset_id:
|
||||
return False, "资产缺少ID信息"
|
||||
# 构建资产的API下载URL
|
||||
download_url = f"https://api.github.com/repos/{user_repo}/releases/assets/{asset_id}"
|
||||
except Exception as e:
|
||||
logger.error(f"解析 Release 信息失败:{e}")
|
||||
return False, f"解析 Release 信息失败:{e}"
|
||||
|
||||
res = self.__request_with_fallback(download_url, headers=settings.REPO_GITHUB_HEADERS(repo=user_repo))
|
||||
# 使用资产的API端点下载,需要设置Accept头为application/octet-stream
|
||||
headers = settings.REPO_GITHUB_HEADERS(repo=user_repo).copy()
|
||||
headers["Accept"] = "application/octet-stream"
|
||||
res = self.__request_with_fallback(download_url, headers=headers, is_api=True)
|
||||
if res is None or res.status_code != 200:
|
||||
return False, f"下载资产失败:{res.status_code if res else '连接失败'}"
|
||||
|
||||
@@ -1525,15 +1530,21 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
asset = next((a for a in assets if a.get("name") == asset_name), None)
|
||||
if not asset:
|
||||
return False, f"未找到资产文件:{asset_name}"
|
||||
download_url = asset.get("browser_download_url")
|
||||
if not download_url:
|
||||
return False, "资产缺少下载地址"
|
||||
asset_id = asset.get("id")
|
||||
if not asset_id:
|
||||
return False, "资产缺少ID信息"
|
||||
# 构建资产的API下载URL
|
||||
download_url = f"https://api.github.com/repos/{user_repo}/releases/assets/{asset_id}"
|
||||
except Exception as e:
|
||||
logger.error(f"解析 Release 信息失败:{e}")
|
||||
return False, f"解析 Release 信息失败:{e}"
|
||||
|
||||
# 使用资产的API端点下载,需要设置Accept头为application/octet-stream
|
||||
headers = settings.REPO_GITHUB_HEADERS(repo=user_repo).copy()
|
||||
headers["Accept"] = "application/octet-stream"
|
||||
res = await self.__async_request_with_fallback(download_url,
|
||||
headers=settings.REPO_GITHUB_HEADERS(repo=user_repo))
|
||||
headers=headers,
|
||||
is_api=True)
|
||||
if res is None or res.status_code != 200:
|
||||
return False, f"下载资产失败:{res.status_code if res else '连接失败'}"
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import os
|
||||
import signal
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
|
||||
@@ -69,6 +73,34 @@ class SystemHelper:
|
||||
logger.debug(f"获取容器ID失败: {str(e)}")
|
||||
return container_id.strip() if container_id else None
|
||||
|
||||
@staticmethod
|
||||
def _check_restart_policy() -> bool:
|
||||
"""
|
||||
检查当前容器是否配置了自动重启策略
|
||||
"""
|
||||
try:
|
||||
# 获取当前容器ID
|
||||
container_id = SystemHelper._get_container_id()
|
||||
if not container_id:
|
||||
return False
|
||||
|
||||
# 创建 Docker 客户端
|
||||
client = docker.DockerClient(base_url=settings.DOCKER_CLIENT_API)
|
||||
# 获取容器信息
|
||||
container = client.containers.get(container_id)
|
||||
restart_policy = container.attrs.get('HostConfig', {}).get('RestartPolicy', {})
|
||||
policy_name = restart_policy.get('Name', 'no')
|
||||
# 检查是否有有效的重启策略
|
||||
auto_restart_policies = ['always', 'unless-stopped', 'on-failure']
|
||||
has_restart_policy = policy_name in auto_restart_policies
|
||||
|
||||
logger.info(f"容器重启策略: {policy_name}, 支持自动重启: {has_restart_policy}")
|
||||
return has_restart_policy
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"检查重启策略失败: {str(e)}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def restart() -> Tuple[bool, str]:
|
||||
"""
|
||||
@@ -77,8 +109,45 @@ class SystemHelper:
|
||||
if not SystemUtils.is_docker():
|
||||
return False, "非Docker环境,无法重启!"
|
||||
|
||||
logger.info("正在重启容器...")
|
||||
return SystemHelper._docker_api_restart()
|
||||
try:
|
||||
# 检查容器是否配置了自动重启策略
|
||||
has_restart_policy = SystemHelper._check_restart_policy()
|
||||
if has_restart_policy:
|
||||
# 有重启策略,使用优雅退出方式
|
||||
logger.info("检测到容器配置了自动重启策略,使用优雅重启方式...")
|
||||
# 启动优雅退出超时监控
|
||||
SystemHelper._start_graceful_shutdown_monitor()
|
||||
# 发送SIGTERM信号给当前进程,触发优雅停止
|
||||
os.kill(os.getpid(), signal.SIGTERM)
|
||||
return True, ""
|
||||
else:
|
||||
# 没有重启策略,使用Docker API强制重启
|
||||
logger.info("容器未配置自动重启策略,使用Docker API重启...")
|
||||
return SystemHelper._docker_api_restart()
|
||||
except Exception as err:
|
||||
logger.error(f"重启失败: {str(err)}")
|
||||
# 降级为Docker API重启
|
||||
logger.warning("降级为Docker API重启...")
|
||||
return SystemHelper._docker_api_restart()
|
||||
|
||||
@staticmethod
|
||||
def _start_graceful_shutdown_monitor():
|
||||
"""
|
||||
启动优雅退出超时监控
|
||||
如果30秒内进程没有退出,则使用Docker API强制重启
|
||||
"""
|
||||
|
||||
def monitor_thread():
|
||||
time.sleep(30) # 等待30秒
|
||||
logger.warning("优雅退出超时30秒,使用Docker API强制重启...")
|
||||
try:
|
||||
SystemHelper._docker_api_restart()
|
||||
except Exception as e:
|
||||
logger.error(f"强制重启失败: {str(e)}")
|
||||
|
||||
# 在后台线程中启动监控
|
||||
thread = threading.Thread(target=monitor_thread, daemon=True)
|
||||
thread.start()
|
||||
|
||||
@staticmethod
|
||||
def _docker_api_restart() -> Tuple[bool, str]:
|
||||
|
||||
@@ -226,11 +226,9 @@ class DoubanApi(metaclass=WeakSingleton):
|
||||
"""
|
||||
处理HTTP响应
|
||||
"""
|
||||
if resp is not None and resp.status_code == 400 and "rate_limit" in resp.text:
|
||||
return resp.json()
|
||||
return resp.json() if resp else {}
|
||||
return resp.json() if resp is not None else None
|
||||
|
||||
@cached(maxsize=settings.CONF.douban, ttl=settings.CONF.meta)
|
||||
@cached(maxsize=settings.CONF.douban, ttl=settings.CONF.meta, skip_none=True)
|
||||
def __invoke(self, url: str, **kwargs) -> dict:
|
||||
"""
|
||||
GET请求
|
||||
@@ -242,7 +240,7 @@ class DoubanApi(metaclass=WeakSingleton):
|
||||
).get_res(url=req_url, params=params)
|
||||
return self._handle_response(resp)
|
||||
|
||||
@cached(maxsize=settings.CONF.douban, ttl=settings.CONF.meta)
|
||||
@cached(maxsize=settings.CONF.douban, ttl=settings.CONF.meta, skip_none=True)
|
||||
async def __async_invoke(self, url: str, **kwargs) -> dict:
|
||||
"""
|
||||
GET请求(异步版本)
|
||||
@@ -265,7 +263,7 @@ class DoubanApi(metaclass=WeakSingleton):
|
||||
params.pop('_ts')
|
||||
return req_url, params
|
||||
|
||||
@cached(maxsize=settings.CONF.douban, ttl=settings.CONF.meta)
|
||||
@cached(maxsize=settings.CONF.douban, ttl=settings.CONF.meta, skip_none=True)
|
||||
def __post(self, url: str, **kwargs) -> dict:
|
||||
"""
|
||||
POST请求
|
||||
@@ -287,7 +285,7 @@ class DoubanApi(metaclass=WeakSingleton):
|
||||
).post_res(url=req_url, data=params)
|
||||
return self._handle_response(resp)
|
||||
|
||||
@cached(maxsize=settings.CONF.douban, ttl=settings.CONF.meta)
|
||||
@cached(maxsize=settings.CONF.douban, ttl=settings.CONF.meta, skip_none=True)
|
||||
async def __async_post(self, url: str, **kwargs) -> dict:
|
||||
"""
|
||||
POST请求(异步版本)
|
||||
|
||||
@@ -251,10 +251,15 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
|
||||
# 检查会话
|
||||
self._check_session()
|
||||
|
||||
resp = self.session.request(
|
||||
method, f"{self.base_url}{endpoint}",
|
||||
**kwargs
|
||||
)
|
||||
try:
|
||||
resp = self.session.request(
|
||||
method, f"{self.base_url}{endpoint}",
|
||||
**kwargs
|
||||
)
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"【阿里云盘】{method} 请求 {endpoint} 网络错误: {str(e)}")
|
||||
return None
|
||||
|
||||
if resp is None:
|
||||
logger.warn(f"【阿里云盘】{method} 请求 {endpoint} 失败!")
|
||||
return None
|
||||
|
||||
@@ -246,6 +246,17 @@ class LocalStorage(StorageBase):
|
||||
logger.error(f"【本地】移动文件失败:{err}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def __should_show_progress(src: Path, dest: Path):
|
||||
"""
|
||||
是否显示进度条
|
||||
"""
|
||||
src_isnetwork = SystemUtils.is_network_filesystem(src)
|
||||
dest_isnetwork = SystemUtils.is_network_filesystem(dest)
|
||||
if src_isnetwork and dest_isnetwork and SystemUtils.is_same_disk(src, dest):
|
||||
return True
|
||||
return False
|
||||
|
||||
def copy(
|
||||
self,
|
||||
fileitem: schemas.FileItem,
|
||||
@@ -258,8 +269,15 @@ class LocalStorage(StorageBase):
|
||||
try:
|
||||
src = Path(fileitem.path)
|
||||
dest = path / new_name
|
||||
if self._copy_with_progress(src, dest):
|
||||
return True
|
||||
if self.__should_show_progress(src, dest):
|
||||
if self._copy_with_progress(src, dest):
|
||||
return True
|
||||
else:
|
||||
code, message = SystemUtils.copy(src, dest)
|
||||
if code == 0:
|
||||
return True
|
||||
else:
|
||||
logger.error(f"【本地】复制文件失败:{message}")
|
||||
except Exception as err:
|
||||
logger.error(f"【本地】复制文件失败:{err}")
|
||||
return False
|
||||
@@ -276,10 +294,17 @@ class LocalStorage(StorageBase):
|
||||
try:
|
||||
src = Path(fileitem.path)
|
||||
dest = path / new_name
|
||||
if self._copy_with_progress(src, dest):
|
||||
# 复制成功删除源文件
|
||||
src.unlink()
|
||||
return True
|
||||
if self.__should_show_progress(src, dest):
|
||||
if self._copy_with_progress(src, dest):
|
||||
# 复制成功删除源文件
|
||||
src.unlink()
|
||||
return True
|
||||
else:
|
||||
code, message = SystemUtils.move(src, dest)
|
||||
if code == 0:
|
||||
return True
|
||||
else:
|
||||
logger.error(f"【本地】移动文件失败:{message}")
|
||||
except Exception as err:
|
||||
logger.error(f"【本地】移动文件失败:{err}")
|
||||
return False
|
||||
|
||||
@@ -205,10 +205,15 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
|
||||
# 检查会话
|
||||
self._check_session()
|
||||
|
||||
resp = self.session.request(
|
||||
method, f"{self.base_url}{endpoint}",
|
||||
**kwargs
|
||||
)
|
||||
try:
|
||||
resp = self.session.request(
|
||||
method, f"{self.base_url}{endpoint}",
|
||||
**kwargs
|
||||
)
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"【115】{method} 请求 {endpoint} 网络错误: {str(e)}")
|
||||
return None
|
||||
|
||||
if resp is None:
|
||||
logger.warn(f"【115】{method} 请求 {endpoint} 失败!")
|
||||
return None
|
||||
|
||||
@@ -85,7 +85,7 @@ class HaiDanSpider:
|
||||
categories = self._movie_category
|
||||
|
||||
# 搜索类型
|
||||
if keyword.startswith('tt'):
|
||||
if keyword and keyword.startswith('tt'):
|
||||
search_area = '4'
|
||||
else:
|
||||
search_area = '0'
|
||||
|
||||
@@ -125,12 +125,12 @@ class TMDb(object):
|
||||
def cache(self, cache):
|
||||
self._cache_enabled = bool(cache)
|
||||
|
||||
@cached(maxsize=settings.CONF.tmdb, ttl=settings.CONF.meta)
|
||||
@cached(maxsize=settings.CONF.tmdb, ttl=settings.CONF.meta, skip_none=True)
|
||||
def cached_request(self, method, url, data, json,
|
||||
_ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.request(method, url, data, json)
|
||||
|
||||
@cached(maxsize=settings.CONF.tmdb, ttl=settings.CONF.meta)
|
||||
@cached(maxsize=settings.CONF.tmdb, ttl=settings.CONF.meta, skip_none=True)
|
||||
async def async_cached_request(self, method, url, data, json,
|
||||
_ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return await self.async_request(method, url, data, json)
|
||||
|
||||
@@ -7,6 +7,8 @@ import json
|
||||
import urllib.parse
|
||||
from http import HTTPStatus
|
||||
|
||||
from app.core.cache import cached
|
||||
from app.core.config import settings
|
||||
from app.utils.http import RequestUtils
|
||||
|
||||
|
||||
@@ -15,7 +17,7 @@ class Auth:
|
||||
TVDB认证类
|
||||
"""
|
||||
|
||||
def __init__(self, url, apikey, pin="", proxy=None, timeout: int = 15):
|
||||
def __init__(self, url: str, apikey: str, pin: str = "", proxy: dict = None, timeout: int = 15):
|
||||
login_info = {"apikey": apikey}
|
||||
if pin != "":
|
||||
login_info["pin"] = pin
|
||||
@@ -35,13 +37,14 @@ class Auth:
|
||||
result = response.json()
|
||||
self.token = result["data"]["token"]
|
||||
else:
|
||||
error_msg = f"登录失败,状态码: {response.status_code if response else 'None'}"
|
||||
if response:
|
||||
if response is not None:
|
||||
try:
|
||||
error_data = response.json()
|
||||
error_msg = f"Code: {response.status_code}, {error_data.get('message', '未知错误')}"
|
||||
except Exception as err:
|
||||
error_msg = f"Code: {response.status_code}, 响应解析失败:{err}"
|
||||
else:
|
||||
error_msg = "网络连接失败,未收到响应"
|
||||
raise Exception(error_msg)
|
||||
except Exception as e:
|
||||
raise Exception(f"TVDB认证失败: {str(e)}")
|
||||
@@ -58,13 +61,14 @@ class Request:
|
||||
请求处理类
|
||||
"""
|
||||
|
||||
def __init__(self, auth_token, proxy=None, timeout=15):
|
||||
def __init__(self, auth_token: str, proxy: dict = None, timeout: int = 15):
|
||||
self.auth_token = auth_token
|
||||
self.links = None
|
||||
self.proxy = proxy
|
||||
self.timeout = timeout
|
||||
|
||||
def make_request(self, url, if_modified_since=None):
|
||||
@cached(maxsize=settings.CONF.tmdb, ttl=settings.CONF.meta, skip_none=True)
|
||||
def make_request(self, url: str, if_modified_since: bool = None):
|
||||
"""
|
||||
向指定的 URL 发起请求并返回数据
|
||||
"""
|
||||
@@ -118,7 +122,8 @@ class Url:
|
||||
def __init__(self):
|
||||
self.base_url = "https://api4.thetvdb.com/v4/"
|
||||
|
||||
def construct(self, url_sect, url_id=None, url_subsect=None, url_lang=None, **kwargs):
|
||||
def construct(self, url_sect: str, url_id: int = None,
|
||||
url_subsect: str = None, url_lang: str = None, **kwargs):
|
||||
"""
|
||||
构建API URL
|
||||
"""
|
||||
@@ -141,7 +146,7 @@ class TVDB:
|
||||
TVDB API主类
|
||||
"""
|
||||
|
||||
def __init__(self, apikey: str, pin="", proxy=None, timeout: int = 15):
|
||||
def __init__(self, apikey: str, pin: str = "", proxy: dict = None, timeout: int = 15):
|
||||
self.url = Url()
|
||||
login_url = self.url.construct("login")
|
||||
self.auth = Auth(login_url, apikey, pin, proxy, timeout)
|
||||
@@ -154,126 +159,126 @@ class TVDB:
|
||||
"""
|
||||
return self.request.links
|
||||
|
||||
def get_artwork_statuses(self, meta=None, if_modified_since=None) -> list:
|
||||
def get_artwork_statuses(self, meta: str = None, if_modified_since: bool = None) -> list:
|
||||
"""
|
||||
返回艺术图状态列表
|
||||
"""
|
||||
url = self.url.construct("artwork/statuses", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_artwork_types(self, meta=None, if_modified_since=None) -> list:
|
||||
def get_artwork_types(self, meta: str = None, if_modified_since: bool = None) -> list:
|
||||
"""
|
||||
返回艺术图类型列表
|
||||
"""
|
||||
url = self.url.construct("artwork/types", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_artwork(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
def get_artwork(self, id: int, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回单个艺术图信息的字典
|
||||
"""
|
||||
url = self.url.construct("artwork", id, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_artwork_extended(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
def get_artwork_extended(self, id: int, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回单个艺术图的扩展信息字典
|
||||
"""
|
||||
url = self.url.construct("artwork", id, "extended", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_all_awards(self, meta=None, if_modified_since=None) -> list:
|
||||
def get_all_awards(self, meta: str = None, if_modified_since: bool = None) -> list:
|
||||
"""
|
||||
返回奖项列表
|
||||
"""
|
||||
url = self.url.construct("awards", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_award(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
def get_award(self, id: int, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回单个奖项信息的字典
|
||||
"""
|
||||
url = self.url.construct("awards", id, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_award_extended(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
def get_award_extended(self, id: int, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回单个奖项的扩展信息字典
|
||||
"""
|
||||
url = self.url.construct("awards", id, "extended", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_all_award_categories(self, meta=None, if_modified_since=None) -> list:
|
||||
def get_all_award_categories(self, meta: str = None, if_modified_since: bool = None) -> list:
|
||||
"""
|
||||
返回奖项类别列表
|
||||
"""
|
||||
url = self.url.construct("awards/categories", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_award_category(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
def get_award_category(self, id: int, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回单个奖项类别信息的字典
|
||||
"""
|
||||
url = self.url.construct("awards/categories", id, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_award_category_extended(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
def get_award_category_extended(self, id: int, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回单个奖项类别的扩展信息字典
|
||||
"""
|
||||
url = self.url.construct("awards/categories", id, "extended", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_content_ratings(self, meta=None, if_modified_since=None) -> list:
|
||||
def get_content_ratings(self, meta: str = None, if_modified_since: bool = None) -> list:
|
||||
"""
|
||||
返回内容分级列表
|
||||
"""
|
||||
url = self.url.construct("content/ratings", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_countries(self, meta=None, if_modified_since=None) -> list:
|
||||
def get_countries(self, meta: str = None, if_modified_since: bool = None) -> list:
|
||||
"""
|
||||
返回国家列表
|
||||
"""
|
||||
url = self.url.construct("countries", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_all_companies(self, page=None, meta=None, if_modified_since=None) -> list:
|
||||
def get_all_companies(self, page: int = None, meta: str = None, if_modified_since: bool = None) -> list:
|
||||
"""
|
||||
返回公司列表 (可分页)
|
||||
"""
|
||||
url = self.url.construct("companies", page=page, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_company_types(self, meta=None, if_modified_since=None) -> list:
|
||||
def get_company_types(self, meta: str = None, if_modified_since: bool = None) -> list:
|
||||
"""
|
||||
返回公司类型列表
|
||||
"""
|
||||
url = self.url.construct("companies/types", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_company(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
def get_company(self, id: int, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回单个公司信息的字典
|
||||
"""
|
||||
url = self.url.construct("companies", id, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_all_series(self, page=None, meta=None, if_modified_since=None) -> list:
|
||||
def get_all_series(self, page: int = None, meta: str = None, if_modified_since: bool = None) -> list:
|
||||
"""
|
||||
返回剧集列表 (可分页)
|
||||
"""
|
||||
url = self.url.construct("series", page=page, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_series(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
def get_series(self, id: int, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回单个剧集信息的字典
|
||||
"""
|
||||
url = self.url.construct("series", id, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_series_by_slug(self, slug: str, meta=None, if_modified_since=None) -> dict:
|
||||
def get_series_by_slug(self, slug: str, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
通过 slug (别名) 返回单个剧集信息的字典
|
||||
"""
|
||||
@@ -288,7 +293,7 @@ class TVDB:
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_series_episodes(self, id: int, season_type: str = "default", page: int = 0,
|
||||
lang: str = None, meta=None, if_modified_since=None, **kwargs) -> dict:
|
||||
lang: str = None, meta: str = None, if_modified_since: bool = None, **kwargs) -> dict:
|
||||
"""
|
||||
返回指定剧集和季类型的各集信息字典 (可分页,可指定语言)
|
||||
"""
|
||||
@@ -297,7 +302,7 @@ class TVDB:
|
||||
)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_series_translation(self, id: int, lang: str, meta=None, if_modified_since=None) -> dict:
|
||||
def get_series_translation(self, id: int, lang: str, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回剧集的指定语言翻译信息字典
|
||||
"""
|
||||
@@ -318,21 +323,21 @@ class TVDB:
|
||||
url = self.url.construct("series", id, "nextAired")
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_all_movies(self, page=None, meta=None, if_modified_since=None) -> list:
|
||||
def get_all_movies(self, page: int = None, meta: str = None, if_modified_since: bool = None) -> list:
|
||||
"""
|
||||
返回电影列表 (可分页)
|
||||
"""
|
||||
url = self.url.construct("movies", page=page, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_movie(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
def get_movie(self, id: int, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回单个电影信息的字典
|
||||
"""
|
||||
url = self.url.construct("movies", id, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_movie_by_slug(self, slug: str, meta=None, if_modified_since=None) -> dict:
|
||||
def get_movie_by_slug(self, slug: str, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
通过 slug (别名) 返回单个电影信息的字典
|
||||
"""
|
||||
@@ -346,70 +351,70 @@ class TVDB:
|
||||
url = self.url.construct("movies", id, "extended", meta=meta, short=short)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_movie_translation(self, id: int, lang: str, meta=None, if_modified_since=None) -> dict:
|
||||
def get_movie_translation(self, id: int, lang: str, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回电影的指定语言翻译信息字典
|
||||
"""
|
||||
url = self.url.construct("movies", id, "translations", lang, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_all_seasons(self, page=None, meta=None, if_modified_since=None) -> list:
|
||||
def get_all_seasons(self, page: int = None, meta: str = None, if_modified_since: bool = None) -> list:
|
||||
"""
|
||||
返回季列表 (可分页)
|
||||
"""
|
||||
url = self.url.construct("seasons", page=page, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_season(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
def get_season(self, id: int, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回单季信息的字典
|
||||
"""
|
||||
url = self.url.construct("seasons", id, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_season_extended(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
def get_season_extended(self, id: int, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回单季的扩展信息字典
|
||||
"""
|
||||
url = self.url.construct("seasons", id, "extended", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_season_types(self, meta=None, if_modified_since=None) -> list:
|
||||
def get_season_types(self, meta: str = None, if_modified_since: bool = None) -> list:
|
||||
"""
|
||||
返回季类型列表
|
||||
"""
|
||||
url = self.url.construct("seasons/types", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_season_translation(self, id: int, lang: str, meta=None, if_modified_since=None) -> dict:
|
||||
def get_season_translation(self, id: int, lang: str, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回季的指定语言翻译信息字典
|
||||
"""
|
||||
url = self.url.construct("seasons", id, "translations", lang, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_all_episodes(self, page=None, meta=None, if_modified_since=None) -> list:
|
||||
def get_all_episodes(self, page: int = None, meta: str = None, if_modified_since: bool = None) -> list:
|
||||
"""
|
||||
返回集列表 (可分页)
|
||||
"""
|
||||
url = self.url.construct("episodes", page=page, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_episode(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
def get_episode(self, id: int, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回单集信息的字典
|
||||
"""
|
||||
url = self.url.construct("episodes", id, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_episode_extended(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
def get_episode_extended(self, id: int, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回单集的扩展信息字典
|
||||
"""
|
||||
url = self.url.construct("episodes", id, "extended", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_episode_translation(self, id: int, lang: str, meta=None, if_modified_since=None) -> dict:
|
||||
def get_episode_translation(self, id: int, lang: str, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回单集的指定语言翻译信息字典
|
||||
"""
|
||||
@@ -419,70 +424,70 @@ class TVDB:
|
||||
# 兼容旧函数名。
|
||||
get_episodes_translation = get_episode_translation
|
||||
|
||||
def get_all_genders(self, meta=None, if_modified_since=None) -> list:
|
||||
def get_all_genders(self, meta: str = None, if_modified_since: bool = None) -> list:
|
||||
"""
|
||||
返回性别列表
|
||||
"""
|
||||
url = self.url.construct("genders", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_all_genres(self, meta=None, if_modified_since=None) -> list:
|
||||
def get_all_genres(self, meta: str = None, if_modified_since: bool = None) -> list:
|
||||
"""
|
||||
返回类型(流派)列表
|
||||
"""
|
||||
url = self.url.construct("genres", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_genre(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
def get_genre(self, id: int, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回单个类型(流派)信息的字典
|
||||
"""
|
||||
url = self.url.construct("genres", id, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_all_languages(self, meta=None, if_modified_since=None) -> list:
|
||||
def get_all_languages(self, meta: str = None, if_modified_since: bool = None) -> list:
|
||||
"""
|
||||
返回语言列表
|
||||
"""
|
||||
url = self.url.construct("languages", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_all_people(self, page=None, meta=None, if_modified_since=None) -> list:
|
||||
def get_all_people(self, page: int = None, meta: str = None, if_modified_since: bool = None) -> list:
|
||||
"""
|
||||
返回人物列表 (可分页)
|
||||
"""
|
||||
url = self.url.construct("people", page=page, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_person(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
def get_person(self, id: int, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回单个人物信息的字典
|
||||
"""
|
||||
url = self.url.construct("people", id, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_person_extended(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
def get_person_extended(self, id: int, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回单个人物的扩展信息字典
|
||||
"""
|
||||
url = self.url.construct("people", id, "extended", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_person_translation(self, id: int, lang: str, meta=None, if_modified_since=None) -> dict:
|
||||
def get_person_translation(self, id: int, lang: str, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回人物的指定语言翻译信息字典
|
||||
"""
|
||||
url = self.url.construct("people", id, "translations", lang, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_character(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
def get_character(self, id: int, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回角色信息的字典
|
||||
"""
|
||||
url = self.url.construct("characters", id, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_people_types(self, meta=None, if_modified_since=None) -> list:
|
||||
def get_people_types(self, meta: str = None, if_modified_since: bool = None) -> list:
|
||||
"""
|
||||
返回人物类型列表
|
||||
"""
|
||||
@@ -492,7 +497,7 @@ class TVDB:
|
||||
# 兼容旧函数名
|
||||
get_all_people_types = get_people_types
|
||||
|
||||
def get_source_types(self, meta=None, if_modified_since=None) -> list:
|
||||
def get_source_types(self, meta: str = None, if_modified_since: bool = None) -> list:
|
||||
"""
|
||||
返回来源类型列表
|
||||
"""
|
||||
@@ -509,56 +514,56 @@ class TVDB:
|
||||
url = self.url.construct("updates", since=since, **kwargs)
|
||||
return self.request.make_request(url)
|
||||
|
||||
def get_all_tag_options(self, page=None, meta=None, if_modified_since=None) -> list:
|
||||
def get_all_tag_options(self, page: int = None, meta: str = None, if_modified_since: bool = None) -> list:
|
||||
"""
|
||||
返回标签选项列表 (可分页)
|
||||
"""
|
||||
url = self.url.construct("tags/options", page=page, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_tag_option(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
def get_tag_option(self, id: int, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回单个标签选项信息的字典
|
||||
"""
|
||||
url = self.url.construct("tags/options", id, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_all_lists(self, page=None, meta=None) -> dict:
|
||||
def get_all_lists(self, page: int = None, meta=None) -> dict:
|
||||
"""
|
||||
返回所有公开的列表信息 (可分页)
|
||||
"""
|
||||
url = self.url.construct("lists", page=page, meta=meta)
|
||||
return self.request.make_request(url)
|
||||
|
||||
def get_list(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
def get_list(self, id: int, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回单个列表信息的字典
|
||||
"""
|
||||
url = self.url.construct("lists", id, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_list_by_slug(self, slug: str, meta=None, if_modified_since=None) -> dict:
|
||||
def get_list_by_slug(self, slug: str, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
通过 slug (别名) 返回单个列表信息的字典
|
||||
"""
|
||||
url = self.url.construct("lists/slug", slug, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_list_extended(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
def get_list_extended(self, id: int, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回单个列表的扩展信息字典
|
||||
"""
|
||||
url = self.url.construct("lists", id, "extended", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_list_translation(self, id: int, lang: str, meta=None, if_modified_since=None) -> dict:
|
||||
def get_list_translation(self, id: int, lang: str, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回列表的指定语言翻译信息字典
|
||||
"""
|
||||
url = self.url.construct("lists", id, "translations", lang, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_inspiration_types(self, meta=None, if_modified_since=None) -> dict:
|
||||
def get_inspiration_types(self, meta: str = None, if_modified_since: bool = None) -> dict:
|
||||
"""
|
||||
返回灵感类型列表
|
||||
"""
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import json
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
@@ -10,13 +9,13 @@ from threading import Lock
|
||||
from typing import Any, Optional, Dict, List
|
||||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from app.core.cache import TTLCache, FileCache
|
||||
from watchdog.events import FileSystemEventHandler, FileSystemMovedEvent, FileSystemEvent
|
||||
from watchdog.observers.polling import PollingObserver
|
||||
|
||||
from app.chain import ChainBase
|
||||
from app.chain.storage import StorageChain
|
||||
from app.chain.transfer import TransferChain
|
||||
from app.core.cache import TTLCache, FileCache
|
||||
from app.core.config import settings
|
||||
from app.core.event import Event, eventmanager
|
||||
from app.helper.directory import DirectoryHelper
|
||||
@@ -26,6 +25,7 @@ from app.schemas import ConfigChangeEventData
|
||||
from app.schemas import FileItem
|
||||
from app.schemas.types import SystemConfigKey, EventType
|
||||
from app.utils.singleton import SingletonClass
|
||||
from app.utils.system import SystemUtils
|
||||
|
||||
lock = Lock()
|
||||
snapshot_lock = Lock()
|
||||
@@ -355,7 +355,8 @@ class Monitor(metaclass=SingletonClass):
|
||||
|
||||
return tips
|
||||
|
||||
def should_use_polling(self, directory: Path, monitor_mode: str,
|
||||
@staticmethod
|
||||
def should_use_polling(directory: Path, monitor_mode: str,
|
||||
file_count: int, limits: dict) -> tuple[bool, str]:
|
||||
"""
|
||||
判断是否应该使用轮询模式
|
||||
@@ -369,7 +370,7 @@ class Monitor(metaclass=SingletonClass):
|
||||
return True, "用户配置为兼容模式"
|
||||
|
||||
# 检查网络文件系统
|
||||
if self.is_network_filesystem(directory):
|
||||
if SystemUtils.is_network_filesystem(directory):
|
||||
return True, "检测到网络文件系统,建议使用兼容模式"
|
||||
|
||||
max_watches = limits.get('max_user_watches')
|
||||
@@ -377,45 +378,6 @@ class Monitor(metaclass=SingletonClass):
|
||||
return True, f"目录文件数量({file_count})接近系统限制({max_watches})"
|
||||
return False, "使用快速模式"
|
||||
|
||||
@staticmethod
|
||||
def is_network_filesystem(directory: Path) -> bool:
|
||||
"""
|
||||
检测是否为网络文件系统
|
||||
:param directory: 目录路径
|
||||
:return: 是否为网络文件系统
|
||||
"""
|
||||
try:
|
||||
system = platform.system()
|
||||
if system == 'Linux':
|
||||
# 检查挂载信息
|
||||
result = subprocess.run(['df', '-T', str(directory)],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
output = result.stdout.lower()
|
||||
# 以下本地文件系统含有fuse关键字
|
||||
local_fs = [
|
||||
"fuse.shfs", # Unraid
|
||||
"zfuse.zfsv", # 极空间(zfuse.zfsv2、zfuse.zfsv3、...)
|
||||
# TBD
|
||||
]
|
||||
if any(fs in output for fs in local_fs):
|
||||
return False
|
||||
network_fs = ['nfs', 'cifs', 'smbfs', 'fuse', 'sshfs', 'ftpfs']
|
||||
return any(fs in output for fs in network_fs)
|
||||
elif system == 'Darwin':
|
||||
# macOS 检查
|
||||
result = subprocess.run(['df', '-T', str(directory)],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
output = result.stdout.lower()
|
||||
return 'nfs' in output or 'smbfs' in output
|
||||
elif system == 'Windows':
|
||||
# Windows 检查网络驱动器
|
||||
return str(directory).startswith('\\\\')
|
||||
except Exception as e:
|
||||
logger.debug(f"检测网络文件系统时出错: {e}")
|
||||
return False
|
||||
|
||||
def init(self):
|
||||
"""
|
||||
启动监控
|
||||
|
||||
@@ -527,6 +527,45 @@ class SystemUtils:
|
||||
print(f"Error occurred: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_network_filesystem(directory: Path) -> bool:
|
||||
"""
|
||||
检测是否为网络文件系统
|
||||
:param directory: 目录路径
|
||||
:return: 是否为网络文件系统
|
||||
"""
|
||||
try:
|
||||
system = platform.system()
|
||||
if system == 'Linux':
|
||||
# 检查挂载信息
|
||||
result = subprocess.run(['df', '-T', str(directory)],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
output = result.stdout.lower()
|
||||
# 以下本地文件系统含有fuse关键字
|
||||
local_fs = [
|
||||
"fuse.shfs", # Unraid
|
||||
"zfuse.zfsv", # 极空间(zfuse.zfsv2、zfuse.zfsv3、...)
|
||||
# TBD
|
||||
]
|
||||
if any(fs in output for fs in local_fs):
|
||||
return False
|
||||
network_fs = ['nfs', 'cifs', 'smbfs', 'fuse', 'sshfs', 'ftpfs']
|
||||
return any(fs in output for fs in network_fs)
|
||||
elif system == 'Darwin':
|
||||
# macOS 检查
|
||||
result = subprocess.run(['df', '-T', str(directory)],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
output = result.stdout.lower()
|
||||
return 'nfs' in output or 'smbfs' in output
|
||||
elif system == 'Windows':
|
||||
# Windows 检查网络驱动器
|
||||
return str(directory).startswith('\\\\')
|
||||
except Exception as e:
|
||||
print(f"Error occurred: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_same_disk(src: Path, dest: Path) -> bool:
|
||||
"""
|
||||
|
||||
@@ -31,23 +31,34 @@ if [ "${ENABLE_SSL}" = "true" ] && \
|
||||
if [ ! -d "/config/acme.sh" ]; then
|
||||
INFO "→ 安装acme.sh..."
|
||||
|
||||
# 生成安装参数
|
||||
INSTALL_ARGS=(
|
||||
"--install-online"
|
||||
"--home" "/config/acme.sh"
|
||||
"--config-home" "/config/acme.sh/data"
|
||||
"--cert-home" "/config/certs"
|
||||
)
|
||||
# 设置安装环境变量
|
||||
export LE_WORKING_DIR="/config/acme.sh"
|
||||
export LE_CONFIG_HOME="/config/acme.sh/data"
|
||||
export LE_CERT_HOME="/config/certs"
|
||||
|
||||
# 添加邮箱参数(如果设置)
|
||||
# 执行官方安装命令(添加错误处理)
|
||||
INFO "正在下载并安装 acme.sh..."
|
||||
|
||||
# 构建安装命令
|
||||
INSTALL_CMD="curl -sSL https://get.acme.sh | sh -s -- --install-online"
|
||||
if [ -n "${SSL_EMAIL}" ]; then
|
||||
INSTALL_ARGS+=("--accountemail" "${SSL_EMAIL}")
|
||||
INSTALL_CMD="${INSTALL_CMD} --accountemail ${SSL_EMAIL}"
|
||||
else
|
||||
WARN "未设置SSL_EMAIL,建议配置邮箱用于证书过期提醒"
|
||||
fi
|
||||
|
||||
if ! eval "${INSTALL_CMD}"; then
|
||||
ERROR "acme.sh 安装失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 执行官方安装命令
|
||||
curl -sSL https://get.acme.sh | sh -s -- "${INSTALL_ARGS[@]}"
|
||||
# 验证安装是否成功
|
||||
if [ ! -f "/config/acme.sh/acme.sh" ]; then
|
||||
ERROR "acme.sh 安装后文件不存在,安装可能失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INFO "acme.sh 安装成功"
|
||||
fi
|
||||
|
||||
# 签发证书(仅当证书不存在时)
|
||||
@@ -77,17 +88,24 @@ if [ "${ENABLE_SSL}" = "true" ] && \
|
||||
fi
|
||||
done
|
||||
|
||||
# 签发证书
|
||||
/config/acme.sh/acme.sh --issue \
|
||||
# 签发证书(添加错误处理)
|
||||
INFO "正在签发证书..."
|
||||
if ! /config/acme.sh/acme.sh --issue \
|
||||
--dns "${DNS_PROVIDER}" \
|
||||
--domain "${SSL_DOMAIN}" \
|
||||
--key-file /config/certs/"${SSL_DOMAIN}"/privkey.pem \
|
||||
--fullchain-file /config/certs/"${SSL_DOMAIN}"/fullchain.pem \
|
||||
--reloadcmd "nginx -s reload" \
|
||||
--force
|
||||
--force; then
|
||||
ERROR "证书签发失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 创建稳定符号链接
|
||||
ln -sf /config/certs/"${SSL_DOMAIN}" /config/certs/latest
|
||||
INFO "证书签发成功"
|
||||
else
|
||||
INFO "证书已存在,跳过签发步骤"
|
||||
fi
|
||||
|
||||
# 配置自动更新任务
|
||||
@@ -98,4 +116,12 @@ if [ "${ENABLE_SSL}" = "true" ] && \
|
||||
|
||||
elif [ "${ENABLE_SSL}" = "true" ] && [ "${AUTO_ISSUE_CERT}" = "true" ] && [ -z "${SSL_DOMAIN}" ]; then
|
||||
WARN "已启用自动签发证书但未设置SSL_DOMAIN,跳过证书管理"
|
||||
elif [ "${ENABLE_SSL}" = "true" ] && [ "${AUTO_ISSUE_CERT}" = "false" ]; then
|
||||
INFO "SSL已启用但自动签发证书已禁用,将使用手动配置的证书"
|
||||
# 检查证书文件是否存在
|
||||
if [ -f "/config/certs/latest/fullchain.pem" ] && [ -f "/config/certs/latest/privkey.pem" ]; then
|
||||
INFO "检测到证书文件,SSL配置正常"
|
||||
else
|
||||
WARN "未检测到证书文件,请确保手动配置了正确的证书路径"
|
||||
fi
|
||||
fi
|
||||
@@ -1,2 +1,2 @@
|
||||
APP_VERSION = 'v2.7.7'
|
||||
FRONTEND_VERSION = 'v2.7.7'
|
||||
APP_VERSION = 'v2.7.8'
|
||||
FRONTEND_VERSION = 'v2.7.8'
|
||||
|
||||
Reference in New Issue
Block a user