refactor(config): retire RuntimeSettingsCompat host usage

This commit is contained in:
jxxghp
2026-08-26 15:55:21 +08:00
parent cdab54254d
commit 9dbe424c3d
162 changed files with 1966 additions and 1745 deletions
+5 -10
View File
@@ -9,19 +9,14 @@ from anyio import Path as AsyncPath
from app.adapters.cache.redis import AsyncRedisHelper, RedisHelper
from app.runtime.cache import (
DEFAULT_CACHE_REGION,
AsyncCacheBackend,
CacheBackend,
DEFAULT_CACHE_REGION,
configure_cache_factories,
)
from app.runtime import config as _runtime_config
from app.runtime.settings import get_runtime_setting
# 兼容旧插件对模块级 Settings 的覆盖,工厂实际读取统一经过 runtime 端口。
settings = _runtime_config.settings
class RedisBackend(CacheBackend):
"""通过同步 Redis 客户端实现缓存后端。"""
@@ -335,14 +330,14 @@ class AsyncFileBackend(AsyncCacheBackend):
def configure_platform_cache() -> None:
"""把配置感知的 Redis 与文件适配器注册到平台缓存工厂。"""
configure_cache_factories(
backend_type_provider=lambda: get_runtime_setting("CACHE_BACKEND_TYPE"),
backend_type_provider=lambda: get_runtime_setting('CACHE_BACKEND_TYPE'),
redis_factory=lambda ttl: RedisBackend(ttl=ttl),
async_redis_factory=lambda ttl: AsyncRedisBackend(ttl=ttl),
file_factory=lambda base: FileBackend(
base=base or get_runtime_setting("TEMP_PATH")
base=base or get_runtime_setting('TEMP_PATH')
),
async_file_factory=lambda base: AsyncFileBackend(
base=base or get_runtime_setting("TEMP_PATH")
base=base or get_runtime_setting('TEMP_PATH')
),
file_ttl_provider=lambda: get_runtime_setting("TEMP_FILE_DAYS") * 24 * 3600,
file_ttl_provider=lambda: get_runtime_setting('TEMP_FILE_DAYS') * 24 * 3600,
)
+16 -21
View File
@@ -2,22 +2,17 @@ import asyncio
import json
import pickle
import threading
from typing import Any, Optional, Generator, Tuple, AsyncGenerator, Union
from typing import Any, AsyncGenerator, Generator, Optional, Tuple, Union
from urllib.parse import quote, unquote
import redis
from redis.asyncio import BlockingConnectionPool as AsyncBlockingConnectionPool
from redis.asyncio import Redis
from app.runtime import config as _runtime_config
from app.foundation.singleton import Singleton
from app.runtime.log import logger
from app.runtime.reload import ConfigReloadMixin
from app.runtime.settings import get_runtime_setting
from app.foundation.singleton import Singleton
# 兼容旧插件和测试对模块级 Settings 的覆盖,Redis 连接逻辑统一读取 runtime 端口。
settings = _runtime_config.settings
# 类型缓存集合,针对非容器简单类型
_complex_serializable_types = set()
@@ -102,7 +97,7 @@ class RedisHelper(ConfigReloadMixin, metaclass=Singleton):
"""
初始化Redis助手实例
"""
self.redis_url = get_runtime_setting("CACHE_BACKEND_URL")
self.redis_url = get_runtime_setting('CACHE_BACKEND_URL')
self.client = None
self._connect_lock = threading.RLock()
@@ -117,15 +112,15 @@ class RedisHelper(ConfigReloadMixin, metaclass=Singleton):
with self._connect_lock:
if self.client is not None:
return
self.redis_url = get_runtime_setting("CACHE_BACKEND_URL")
self.redis_url = get_runtime_setting('CACHE_BACKEND_URL')
connection_pool = redis.BlockingConnectionPool.from_url(
self.redis_url,
decode_responses=False,
socket_timeout=_socket_timeout,
socket_connect_timeout=_socket_connect_timeout,
health_check_interval=_health_check_interval,
max_connections=get_runtime_setting("CACHE_REDIS_MAX_CONNECTIONS"),
timeout=get_runtime_setting("CACHE_REDIS_POOL_TIMEOUT"),
max_connections=get_runtime_setting('CACHE_REDIS_MAX_CONNECTIONS'),
timeout=get_runtime_setting('CACHE_REDIS_POOL_TIMEOUT'),
)
client = redis.Redis(connection_pool=connection_pool)
# 测试连接,确保Redis可用
@@ -143,7 +138,7 @@ class RedisHelper(ConfigReloadMixin, metaclass=Singleton):
def on_config_changed(self):
"""缓存配置变化后重建同步 Redis 连接。"""
with self._connect_lock:
self.redis_url = get_runtime_setting("CACHE_BACKEND_URL")
self.redis_url = get_runtime_setting('CACHE_BACKEND_URL')
self.close()
self._connect()
@@ -159,8 +154,8 @@ class RedisHelper(ConfigReloadMixin, metaclass=Singleton):
"""
try:
# 如果有显式值,则直接使用,为0时说明不限制,如果未配置,开启BIG_MEMORY_MODE时为"1024mb",未开启时为"256mb"
maxmemory = get_runtime_setting("CACHE_REDIS_MAXMEMORY") or (
"1024mb" if get_runtime_setting("BIG_MEMORY_MODE") else "256mb"
maxmemory = get_runtime_setting('CACHE_REDIS_MAXMEMORY') or (
"1024mb" if get_runtime_setting('BIG_MEMORY_MODE') else "256mb"
)
self.client.config_set("maxmemory", maxmemory)
self.client.config_set("maxmemory-policy", policy)
@@ -371,7 +366,7 @@ class AsyncRedisHelper(ConfigReloadMixin, metaclass=Singleton):
"""
初始化异步Redis助手实例
"""
self.redis_url = get_runtime_setting("CACHE_BACKEND_URL")
self.redis_url = get_runtime_setting('CACHE_BACKEND_URL')
self.client: Optional[Redis] = None
self._loop: Optional[asyncio.AbstractEventLoop] = None
self._connect_lock: Optional[asyncio.Lock] = None
@@ -401,15 +396,15 @@ class AsyncRedisHelper(ConfigReloadMixin, metaclass=Singleton):
await self._close_client()
if self.client is not None:
return
self.redis_url = get_runtime_setting("CACHE_BACKEND_URL")
self.redis_url = get_runtime_setting('CACHE_BACKEND_URL')
connection_pool = AsyncBlockingConnectionPool.from_url(
self.redis_url,
decode_responses=False,
socket_timeout=_socket_timeout,
socket_connect_timeout=_socket_connect_timeout,
health_check_interval=_health_check_interval,
max_connections=get_runtime_setting("CACHE_REDIS_MAX_CONNECTIONS"),
timeout=get_runtime_setting("CACHE_REDIS_POOL_TIMEOUT"),
max_connections=get_runtime_setting('CACHE_REDIS_MAX_CONNECTIONS'),
timeout=get_runtime_setting('CACHE_REDIS_POOL_TIMEOUT'),
)
client = Redis(connection_pool=connection_pool)
self._loop = current_loop
@@ -440,7 +435,7 @@ class AsyncRedisHelper(ConfigReloadMixin, metaclass=Singleton):
async def on_config_changed(self):
"""缓存配置变化后异步重建 Redis 连接。"""
self.redis_url = get_runtime_setting("CACHE_BACKEND_URL")
self.redis_url = get_runtime_setting('CACHE_BACKEND_URL')
await self._close_client()
await self._connect()
@@ -456,8 +451,8 @@ class AsyncRedisHelper(ConfigReloadMixin, metaclass=Singleton):
"""
try:
# 如果有显式值,则直接使用,为0时说明不限制,如果未配置,开启BIG_MEMORY_MODE时为"1024mb",未开启时为"256mb"
maxmemory = get_runtime_setting("CACHE_REDIS_MAXMEMORY") or (
"1024mb" if get_runtime_setting("BIG_MEMORY_MODE") else "256mb"
maxmemory = get_runtime_setting('CACHE_REDIS_MAXMEMORY') or (
"1024mb" if get_runtime_setting('BIG_MEMORY_MODE') else "256mb"
)
await self.client.config_set("maxmemory", maxmemory)
await self.client.config_set("maxmemory-policy", policy)
+62 -63
View File
@@ -37,7 +37,7 @@ from app.runtime.dependencies import (
iter_runtime_requirement_strings,
runtime_excluded_dependency_pairs,
)
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
from app.adapters.system.package import (
PackageInstallRequest,
build_package_install_strategies,
@@ -64,9 +64,8 @@ from app.adapters.system.host import SystemUtils
from app.foundation.url import UrlUtils
from app.runtime.version import get_app_version
# 保留模块级可替换入口,代理默认读取组合根的最新 runtime 配置。
settings = RuntimeSettingsCompat()
PLUGIN_DIR = Path(settings.ROOT_PATH) / "app" / "plugins"
# 插件市场只通过 runtime 读取端口消费组合根的最新配置。
PLUGIN_DIR = Path(get_runtime_setting('ROOT_PATH')) / "app" / "plugins"
LOCAL_REPO_PREFIX = "local://"
PLUGIN_SYSTEM_VERSION_FIELD = "system_version"
PLUGIN_MARKET_WIKI_START = "<!-- plugin-market-repos:start -->"
@@ -317,7 +316,7 @@ class PluginHelper(metaclass=WeakSingleton):
return None
path = Path(values[0]).expanduser()
if not path.is_absolute():
path = settings.ROOT_PATH / path
path = get_runtime_setting('ROOT_PATH') / path
return path.resolve()
except Exception:
return None
@@ -355,9 +354,9 @@ class PluginHelper(metaclass=WeakSingleton):
未启用 VERSION_FLAG(v1)时返回空列表,表示仅使用 package.json 基础索引。
"""
flags: List[str] = []
if settings.VERSION_FLAG:
flags.append(settings.VERSION_FLAG)
flags.extend(VERSION_BACKWARD_COMPATIBLE_FLAGS.get(settings.VERSION_FLAG, []))
if get_runtime_setting('VERSION_FLAG'):
flags.append(get_runtime_setting('VERSION_FLAG'))
flags.extend(VERSION_BACKWARD_COMPATIBLE_FLAGS.get(get_runtime_setting('VERSION_FLAG'), []))
return flags
@classmethod
@@ -370,9 +369,9 @@ class PluginHelper(metaclass=WeakSingleton):
"""
if not isinstance(plugin_info, dict):
return False
if not settings.VERSION_FLAG:
if not get_runtime_setting('VERSION_FLAG'):
return True
current_flag = settings.VERSION_FLAG
current_flag = get_runtime_setting('VERSION_FLAG')
if plugin_info.get(current_flag) is False:
return False
if plugin_info.get(current_flag) is True:
@@ -398,7 +397,7 @@ class PluginHelper(metaclass=WeakSingleton):
"""
if not isinstance(plugin_info, dict):
return False
current_flag = settings.VERSION_FLAG
current_flag = get_runtime_setting('VERSION_FLAG')
if not current_flag:
return not package_version
if package_version == current_flag:
@@ -416,7 +415,7 @@ class PluginHelper(metaclass=WeakSingleton):
package_version: Optional[str],
) -> Tuple[str, ...]:
"""返回插件安装唯一的代际候选顺序,并去除重复的基础索引。"""
preferred_version = package_version or settings.VERSION_FLAG
preferred_version = package_version or get_runtime_setting('VERSION_FLAG')
candidates = [preferred_version]
candidates.extend(
VERSION_BACKWARD_COMPATIBLE_FLAGS.get(preferred_version, [])
@@ -491,16 +490,16 @@ class PluginHelper(metaclass=WeakSingleton):
"""
获取本地插件仓库目录列表
"""
if not settings.PLUGIN_LOCAL_REPO_PATHS:
if not get_runtime_setting('PLUGIN_LOCAL_REPO_PATHS'):
return []
paths = []
for item in settings.PLUGIN_LOCAL_REPO_PATHS.split(","):
for item in get_runtime_setting('PLUGIN_LOCAL_REPO_PATHS').split(","):
local_repo_path = item.strip()
if not local_repo_path:
continue
path = Path(local_repo_path).expanduser()
if not path.is_absolute():
path = settings.ROOT_PATH / path
path = get_runtime_setting('ROOT_PATH') / path
paths.append(path.resolve())
return paths
@@ -542,11 +541,11 @@ class PluginHelper(metaclass=WeakSingleton):
continue
package_candidates = []
if settings.VERSION_FLAG:
package_candidates.append((settings.VERSION_FLAG, self.__get_local_package(repo_path,
settings.VERSION_FLAG)))
if get_runtime_setting('VERSION_FLAG'):
package_candidates.append((get_runtime_setting('VERSION_FLAG'), self.__get_local_package(repo_path,
get_runtime_setting('VERSION_FLAG'))))
# 向后兼容:补充扫描更低版本的 package 文件,便于本地仓库复用历史版本插件。
for backward_flag in VERSION_BACKWARD_COMPATIBLE_FLAGS.get(settings.VERSION_FLAG, []):
for backward_flag in VERSION_BACKWARD_COMPATIBLE_FLAGS.get(get_runtime_setting('VERSION_FLAG'), []):
package_candidates.append((backward_flag, self.__get_local_package(repo_path, backward_flag)))
package_candidates.append(("", self.__get_local_package(repo_path)))
@@ -611,9 +610,9 @@ class PluginHelper(metaclass=WeakSingleton):
repo_paths = [repo_path.resolve()] if repo_path else self.get_local_repo_paths()
package_versions = [package_version] if package_version is not None else []
if package_version is None:
if settings.VERSION_FLAG:
package_versions.append(settings.VERSION_FLAG)
package_versions.extend(VERSION_BACKWARD_COMPATIBLE_FLAGS.get(settings.VERSION_FLAG, []))
if get_runtime_setting('VERSION_FLAG'):
package_versions.append(get_runtime_setting('VERSION_FLAG'))
package_versions.extend(VERSION_BACKWARD_COMPATIBLE_FLAGS.get(get_runtime_setting('VERSION_FLAG'), []))
package_versions.append("")
selected_candidate = None
for repo_order, local_repo_path in enumerate(self.get_local_repo_paths()):
@@ -650,7 +649,7 @@ class PluginHelper(metaclass=WeakSingleton):
if not is_compatible:
candidate["compatible"] = False
candidate["skip_reason"] = (
f"插件索引条目不兼容 {settings.VERSION_FLAG}"
f"插件索引条目不兼容 {get_runtime_setting('VERSION_FLAG')}"
)
self.annotate_plugin_system_version(candidate)
if strict_system_version and candidate.get("system_version_compatible") is False:
@@ -729,7 +728,7 @@ class PluginHelper(metaclass=WeakSingleton):
else "package.json"
)
package_url = cls.__append_cache_buster(f"{raw_url}{package_file}")
headers = settings.REPO_GITHUB_HEADERS(repo=f"{user}/{repo}")
headers = get_runtime_setting('REPO_GITHUB_HEADERS')(repo=f"{user}/{repo}")
return package_url, headers
@classmethod
@@ -829,7 +828,7 @@ class PluginHelper(metaclass=WeakSingleton):
return
user_repo = f"{user}/{repo}"
headers = settings.REPO_GITHUB_HEADERS(repo=user_repo)
headers = get_runtime_setting('REPO_GITHUB_HEADERS')(repo=user_repo)
for page in range(1, 11):
release_api = (
f"https://api.github.com/repos/{user_repo}/releases"
@@ -998,7 +997,7 @@ class PluginHelper(metaclass=WeakSingleton):
package_version: Optional[str] = None) -> Optional[str]:
"""
检查并获取指定插件的可用版本,支持多版本优先级加载和版本兼容性检测
1. 如果未指定版本,则使用系统配置的默认版本(通过 settings.VERSION_FLAG 设置)
1. 如果未指定版本,则使用系统配置的默认版本(通过 get_runtime_setting('VERSION_FLAG') 设置)
2. 优先检查指定版本的插件(如 `package.v2.json`
3. 检查更低版本的 package 文件,并应用版本兼容标志
4. 检查 `package.json` 文件,并应用共享实现兼容标志
@@ -1084,7 +1083,7 @@ class PluginHelper(metaclass=WeakSingleton):
user_repo = f"{user}/{repo}"
if not package_version:
package_version = settings.VERSION_FLAG
package_version = get_runtime_setting('VERSION_FLAG')
# 1. 优先检查指定版本的插件
package_version = self.get_plugin_package_version(pid, repo_url, package_version)
@@ -1232,7 +1231,7 @@ class PluginHelper(metaclass=WeakSingleton):
file_api += f"/{pid.lower()}"
res = self.__request_with_fallback(file_api,
headers=settings.REPO_GITHUB_HEADERS(repo=user_repo),
headers=get_runtime_setting('REPO_GITHUB_HEADERS')(repo=user_repo),
is_api=True,
timeout=30)
if res is None:
@@ -1273,7 +1272,7 @@ class PluginHelper(metaclass=WeakSingleton):
if item.get("download_url"):
logger.debug(f"正在下载文件:{item.get('path')}")
res = self.__request_with_fallback(item.get('download_url'),
headers=settings.REPO_GITHUB_HEADERS(repo=user_repo))
headers=get_runtime_setting('REPO_GITHUB_HEADERS')(repo=user_repo))
if not res:
return False, f"文件 {item.get('path')} 下载失败!"
elif res.status_code != 200:
@@ -1285,7 +1284,7 @@ class PluginHelper(metaclass=WeakSingleton):
relative_path = relative_path.replace(f"plugins.{package_version}", "plugins", 1)
# 创建插件文件夹并写入文件
file_path = Path(settings.ROOT_PATH) / "app" / relative_path
file_path = Path(get_runtime_setting('ROOT_PATH')) / "app" / relative_path
file_path.parent.mkdir(parents=True, exist_ok=True)
with open(file_path, "w", encoding="utf-8") as f:
f.write(res.text)
@@ -1327,7 +1326,7 @@ class PluginHelper(metaclass=WeakSingleton):
:return: 备份目录路径
"""
plugin_dir = PLUGIN_DIR / pid.lower()
backup_dir = Path(settings.TEMP_PATH) / "plugin_backup" / pid.lower()
backup_dir = Path(get_runtime_setting('TEMP_PATH')) / "plugin_backup" / pid.lower()
if plugin_dir.exists():
# 备份时清理已有的备份目录,防止残留文件影响
@@ -1381,7 +1380,7 @@ class PluginHelper(metaclass=WeakSingleton):
logger.warn(f"{pid} 插件目录不存在,跳过刷新插件备份")
return False
backup_root = settings.CONFIG_PATH / "plugins_backup"
backup_root = get_runtime_setting('CONFIG_PATH') / "plugins_backup"
backup_dir = backup_root / pid.lower()
staging_dir = backup_root / f".{pid.lower()}.tmp-{uuid.uuid4().hex}"
previous_dir = backup_root / f".{pid.lower()}.old-{uuid.uuid4().hex}"
@@ -1546,7 +1545,7 @@ class PluginHelper(metaclass=WeakSingleton):
if package_name in cls._protected_runtime_packages
}
project_file = settings.ROOT_PATH / "pyproject.toml"
project_file = get_runtime_setting('ROOT_PATH') / "pyproject.toml"
root_requirements = cls.__parse_project_requirement_roots(project_file)
if not root_requirements:
return protected_packages
@@ -1595,7 +1594,7 @@ class PluginHelper(metaclass=WeakSingleton):
def __get_strict_runtime_packages(cls) -> Set[str]:
"""返回核心包及当前 ABI profile 中不得被插件改写的根包。"""
packages = set(cls._protected_runtime_packages)
project_file = settings.ROOT_PATH / "pyproject.toml"
project_file = get_runtime_setting('ROOT_PATH') / "pyproject.toml"
try:
for raw_requirement in iter_runtime_profile_requirement_strings(project_file):
requirement = Requirement(raw_requirement)
@@ -1717,7 +1716,7 @@ class PluginHelper(metaclass=WeakSingleton):
"""
以主程序依赖的当前已安装版本生成临时约束文件,确保插件安装不会改写主程序依赖。
"""
temp_dir = Path(settings.TEMP_PATH) / "plugin_dependencies"
temp_dir = Path(get_runtime_setting('TEMP_PATH')) / "plugin_dependencies"
temp_dir.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile(
mode="w",
@@ -1794,10 +1793,10 @@ class PluginHelper(metaclass=WeakSingleton):
python_bin=Path(sys.executable),
find_links_dirs=find_links_dirs or [],
constraints_file=constraints_file,
config_dir=settings.CONFIG_PATH,
package_cache_root=settings.PACKAGE_CACHE_PATH,
package_index_url=settings.PIP_PROXY or None,
proxy_url=settings.PROXY_HOST or None,
config_dir=get_runtime_setting('CONFIG_PATH'),
package_cache_root=get_runtime_setting('PACKAGE_CACHE_PATH'),
package_index_url=get_runtime_setting('PIP_PROXY') or None,
proxy_url=get_runtime_setting('PROXY_HOST') or None,
purpose=purpose,
)
@@ -1868,7 +1867,7 @@ class PluginHelper(metaclass=WeakSingleton):
return lines
excluded_pairs = runtime_excluded_dependency_pairs(
Path(settings.ROOT_PATH) / "pyproject.toml"
Path(get_runtime_setting('ROOT_PATH')) / "pyproject.toml"
)
package_errors = set()
for match in matches:
@@ -1930,12 +1929,12 @@ class PluginHelper(metaclass=WeakSingleton):
if repair_target and not repair_target.exists():
repair_target = None
if repair_target is None:
repair_target = settings.ROOT_PATH / "pyproject.toml"
repair_target = get_runtime_setting('ROOT_PATH') / "pyproject.toml"
repair_desc = "主程序 uv.lock"
if not repair_target.exists():
return False, f"恢复依赖文件不存在:{repair_target}"
if snapshot_file is None and not (settings.ROOT_PATH / "uv.lock").exists():
return False, f"恢复依赖文件不存在:{settings.ROOT_PATH / 'uv.lock'}"
if snapshot_file is None and not (get_runtime_setting('ROOT_PATH') / "uv.lock").exists():
return False, f"恢复依赖文件不存在:{get_runtime_setting('ROOT_PATH') / 'uv.lock'}"
last_error = ""
request = cls.__build_package_install_request(repair_target, purpose="runtime-repair")
@@ -2127,21 +2126,21 @@ class PluginHelper(metaclass=WeakSingleton):
) -> List[Tuple[str, str, dict]]:
"""构造同步与异步 GitHub 请求共用的镜像、代理和直连顺序。"""
strategies: List[Tuple[str, str, dict]] = []
if not is_api and settings.GITHUB_PROXY:
if not is_api and get_runtime_setting('GITHUB_PROXY'):
proxy_url = (
f"{UrlUtils.standardize_base_url(settings.GITHUB_PROXY)}{url}"
f"{UrlUtils.standardize_base_url(get_runtime_setting('GITHUB_PROXY'))}{url}"
)
strategies.append(
("镜像站", proxy_url, {"headers": headers, "timeout": timeout})
)
if settings.PROXY_HOST:
if get_runtime_setting('PROXY_HOST'):
strategies.append(
(
"代理",
url,
{
"headers": headers,
"proxies": settings.PROXY,
"proxies": get_runtime_setting('PROXY'),
"timeout": timeout,
},
)
@@ -2218,7 +2217,7 @@ class PluginHelper(metaclass=WeakSingleton):
compatible, message = self.check_plugin_system_version(candidate)
return None if compatible else message
package_version = self.get_plugin_package_version(pid, repo_url, settings.VERSION_FLAG)
package_version = self.get_plugin_package_version(pid, repo_url, get_runtime_setting('VERSION_FLAG'))
if package_version is None:
return None
meta = self.__get_plugin_meta(pid, repo_url, package_version)
@@ -2235,7 +2234,7 @@ class PluginHelper(metaclass=WeakSingleton):
if self.is_local_repo_url(repo_url):
return await asyncio.to_thread(self.get_plugin_system_version_check_message, pid, repo_url)
package_version = await self.async_get_plugin_package_version(pid, repo_url, settings.VERSION_FLAG)
package_version = await self.async_get_plugin_package_version(pid, repo_url, get_runtime_setting('VERSION_FLAG'))
if package_version is None:
return None
meta = await self.__async_get_plugin_meta(pid, repo_url, package_version)
@@ -2382,7 +2381,7 @@ class PluginHelper(metaclass=WeakSingleton):
release_api = f"https://api.github.com/repos/{user_repo}/releases/tags/{release_tag}"
rel_res = self.__request_with_fallback(
release_api,
headers=settings.REPO_GITHUB_HEADERS(repo=user_repo),
headers=get_runtime_setting('REPO_GITHUB_HEADERS')(repo=user_repo),
timeout=30,
is_api=True,
)
@@ -2405,7 +2404,7 @@ class PluginHelper(metaclass=WeakSingleton):
return False, f"解析 Release 信息失败:{e}"
# 使用资产的API端点下载,需要设置Accept头为application/octet-stream
headers = settings.REPO_GITHUB_HEADERS(repo=user_repo).copy()
headers = get_runtime_setting('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:
@@ -2416,7 +2415,7 @@ class PluginHelper(metaclass=WeakSingleton):
infos = zf.infolist()
if not infos:
return False, "压缩包内容为空"
dest_base = Path(settings.ROOT_PATH) / "app" / "plugins" / pid.lower()
dest_base = Path(get_runtime_setting('ROOT_PATH')) / "app" / "plugins" / pid.lower()
targets = self.__iter_release_zip_targets(zf, dest_base)
wrote_any = False
for info, dest_path, is_dir in targets:
@@ -2721,7 +2720,7 @@ class PluginHelper(metaclass=WeakSingleton):
file_api += f"/{pid.lower()}"
res = await self.__async_request_with_fallback(file_api,
headers=settings.REPO_GITHUB_HEADERS(repo=user_repo),
headers=get_runtime_setting('REPO_GITHUB_HEADERS')(repo=user_repo),
is_api=True,
timeout=30)
if res is None:
@@ -2762,7 +2761,7 @@ class PluginHelper(metaclass=WeakSingleton):
if item.get("download_url"):
logger.debug(f"正在下载文件:{item.get('path')}")
res = await self.__async_request_with_fallback(item.get('download_url'),
headers=settings.REPO_GITHUB_HEADERS(repo=user_repo))
headers=get_runtime_setting('REPO_GITHUB_HEADERS')(repo=user_repo))
if not res:
return False, f"文件 {item.get('path')} 下载失败!"
elif res.status_code != 200:
@@ -2774,7 +2773,7 @@ class PluginHelper(metaclass=WeakSingleton):
relative_path = relative_path.replace(f"plugins.{package_version}", "plugins", 1)
# 创建插件文件夹并写入文件
file_path = AsyncPath(settings.ROOT_PATH) / "app" / relative_path
file_path = AsyncPath(get_runtime_setting('ROOT_PATH')) / "app" / relative_path
await file_path.parent.mkdir(parents=True, exist_ok=True)
async with aiofiles.open(file_path, "w", encoding="utf-8") as f:
await f.write(res.text)
@@ -2825,13 +2824,13 @@ class PluginHelper(metaclass=WeakSingleton):
if repair_target and not await _await_thread_operation(repair_target.exists):
repair_target = None
if repair_target is None:
repair_target = settings.ROOT_PATH / "pyproject.toml"
repair_target = get_runtime_setting('ROOT_PATH') / "pyproject.toml"
repair_desc = "主程序 uv.lock"
if not await _await_thread_operation(repair_target.exists):
return False, f"恢复依赖文件不存在:{repair_target}"
lock_file = settings.ROOT_PATH / "uv.lock"
lock_file = get_runtime_setting('ROOT_PATH') / "uv.lock"
if snapshot_file is None and not await _await_thread_operation(lock_file.exists):
return False, f"恢复依赖文件不存在:{settings.ROOT_PATH / 'uv.lock'}"
return False, f"恢复依赖文件不存在:{get_runtime_setting('ROOT_PATH') / 'uv.lock'}"
request = cls.__build_package_install_request(
repair_target,
@@ -3076,7 +3075,7 @@ class PluginHelper(metaclass=WeakSingleton):
:return: 备份目录路径
"""
plugin_dir = AsyncPath(PLUGIN_DIR) / pid.lower()
backup_dir = AsyncPath(settings.TEMP_PATH) / "plugin_backup" / pid.lower()
backup_dir = AsyncPath(get_runtime_setting('TEMP_PATH')) / "plugin_backup" / pid.lower()
if await plugin_dir.exists():
try:
@@ -3234,7 +3233,7 @@ class PluginHelper(metaclass=WeakSingleton):
user_repo = f"{user}/{repo}"
if not package_version:
package_version = settings.VERSION_FLAG
package_version = get_runtime_setting('VERSION_FLAG')
# 1. 优先检查指定版本的插件
package_version = await self.async_get_plugin_package_version(pid, repo_url, package_version)
@@ -3396,7 +3395,7 @@ class PluginHelper(metaclass=WeakSingleton):
release_api = f"https://api.github.com/repos/{user_repo}/releases/tags/{release_tag}"
rel_res = await self.__async_request_with_fallback(
release_api,
headers=settings.REPO_GITHUB_HEADERS(repo=user_repo),
headers=get_runtime_setting('REPO_GITHUB_HEADERS')(repo=user_repo),
timeout=30,
is_api=True,
)
@@ -3419,7 +3418,7 @@ class PluginHelper(metaclass=WeakSingleton):
return False, f"解析 Release 信息失败:{e}"
# 使用资产的API端点下载,需要设置Accept头为application/octet-stream
headers = settings.REPO_GITHUB_HEADERS(repo=user_repo).copy()
headers = get_runtime_setting('REPO_GITHUB_HEADERS')(repo=user_repo).copy()
headers["Accept"] = "application/octet-stream"
res = await self.__async_request_with_fallback(download_url,
headers=headers,
@@ -3432,7 +3431,7 @@ class PluginHelper(metaclass=WeakSingleton):
infos = zf.infolist()
if not infos:
return False, "压缩包内容为空"
dest_base = Path(settings.ROOT_PATH) / "app" / "plugins" / pid.lower()
dest_base = Path(get_runtime_setting('ROOT_PATH')) / "app" / "plugins" / pid.lower()
targets = self.__iter_release_zip_targets(zf, dest_base)
wrote_any = False
for info, dest_path, is_dir in targets:
+1 -1
View File
@@ -13,7 +13,7 @@ class OcrHelper:
def __init__(self, ocr_base_url: Optional[str] = None) -> None:
"""初始化 OCR 服务地址,优先使用组合根设置快照。"""
if ocr_base_url is None:
ocr_base_url = get_runtime_setting("OCR_HOST")
ocr_base_url = get_runtime_setting('OCR_HOST')
self._ocr_b64_url = f"{str(ocr_base_url).rstrip('/')}/captcha/base64"
def get_captcha_text(
+67 -68
View File
@@ -7,7 +7,7 @@ from urllib.parse import parse_qs, quote, urlparse, urlsplit
from app.runtime.cache import cached
from app.runtime.config import global_vars
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
from app.runtime.tasks import get_task_registry
from app.domain.context import MediaInfo, MusicInfo
from app.domain.meta.metabase import MetaBase
@@ -26,8 +26,7 @@ from app.adapters.system.host import SystemUtils
from app.runtime.version import get_app_version, get_frontend_version
# 保留旧插件可覆盖的模块级入口,默认通过 runtime 代理动态读取配置。
settings = RuntimeSettingsCompat()
# 中心服务适配器只通过 runtime 读取端口消费组合根的最新配置。
_server_report_service: Any = None
@@ -102,7 +101,7 @@ class MoviePilotServerHelper:
"""
判断请求地址是否指向配置中的 MoviePilot 服务端。
"""
server_host = (settings.MP_SERVER_HOST or "").strip().rstrip("/")
server_host = (get_runtime_setting('MP_SERVER_HOST') or "").strip().rstrip("/")
if not server_host or not url:
return False
@@ -144,7 +143,7 @@ class MoviePilotServerHelper:
user_uid = cls.get_user_uid()
if user_uid:
request_headers[cls.USER_UID_HEADER] = user_uid
request_headers["User-Agent"] = settings.USER_AGENT
request_headers["User-Agent"] = get_runtime_setting('USER_AGENT')
return request_headers
@classmethod
@@ -162,10 +161,10 @@ class MoviePilotServerHelper:
"""
获取当前 GitHub 用户名。
"""
if cls._github_user is None and settings.GITHUB_HEADERS:
if cls._github_user is None and get_runtime_setting('GITHUB_HEADERS'):
res = RequestUtils(
headers=settings.GITHUB_HEADERS,
proxies=settings.PROXY,
headers=get_runtime_setting('GITHUB_HEADERS'),
proxies=get_runtime_setting('PROXY'),
timeout=15,
).get_res("https://api.github.com/user")
if res:
@@ -178,10 +177,10 @@ class MoviePilotServerHelper:
"""
异步获取当前 GitHub 用户名。
"""
if cls._github_user is None and settings.GITHUB_HEADERS:
if cls._github_user is None and get_runtime_setting('GITHUB_HEADERS'):
res = await AsyncRequestUtils(
headers=settings.GITHUB_HEADERS,
proxies=settings.PROXY,
headers=get_runtime_setting('GITHUB_HEADERS'),
proxies=get_runtime_setting('PROXY'),
timeout=15,
).get_res("https://api.github.com/user")
if res:
@@ -278,7 +277,7 @@ class MoviePilotServerHelper:
"user_uid": cls.get_user_uid(),
"backend_version": get_app_version(),
"frontend_version": get_frontend_version(),
"version_flag": settings.VERSION_FLAG,
"version_flag": get_runtime_setting('VERSION_FLAG'),
"platform": f"{platform.system()} {platform.release()}".strip(),
"arch": SystemUtils.cpu_arch(),
}
@@ -288,7 +287,7 @@ class MoviePilotServerHelper:
"""
上报当前安装实例的版本统计。
"""
if not settings.USAGE_STATISTIC_SHARE:
if not get_runtime_setting('USAGE_STATISTIC_SHARE'):
return False
payload = cls.build_usage_payload()
if not payload.get("user_uid"):
@@ -305,7 +304,7 @@ class MoviePilotServerHelper:
"""
异步上报当前安装实例的版本统计。
"""
if not settings.USAGE_STATISTIC_SHARE:
if not get_runtime_setting('USAGE_STATISTIC_SHARE'):
return False
payload = cls.build_usage_payload()
if not payload.get("user_uid"):
@@ -322,7 +321,7 @@ class MoviePilotServerHelper:
"""
异步获取安装版本统计报表。
"""
if not settings.USAGE_STATISTIC_SHARE:
if not get_runtime_setting('USAGE_STATISTIC_SHARE'):
return {}
try:
res = await cls.async_usage_statistic()
@@ -338,7 +337,7 @@ class MoviePilotServerHelper:
初始化订阅统计上报状态。
"""
cls._report_service().init_report(
enabled=settings.SUBSCRIBE_STATISTIC_SHARE,
enabled=get_runtime_setting('SUBSCRIBE_STATISTIC_SHARE'),
state_key=SystemConfigKey.SubscribeReport,
reporter=cls.sub_report,
)
@@ -347,7 +346,7 @@ class MoviePilotServerHelper:
async def async_init_subscribe_report(cls) -> None:
"""异步初始化订阅统计标记。"""
await cls._report_service().async_init_report(
enabled=settings.SUBSCRIBE_STATISTIC_SHARE,
enabled=get_runtime_setting('SUBSCRIBE_STATISTIC_SHARE'),
state_key=SystemConfigKey.SubscribeReport,
reporter=cls.async_sub_report,
)
@@ -358,7 +357,7 @@ class MoviePilotServerHelper:
初始化插件安装统计上报状态。
"""
cls._report_service().init_report(
enabled=settings.PLUGIN_STATISTIC_SHARE,
enabled=get_runtime_setting('PLUGIN_STATISTIC_SHARE'),
state_key=SystemConfigKey.PluginInstallReport,
reporter=cls.install_plugin_report,
)
@@ -367,7 +366,7 @@ class MoviePilotServerHelper:
async def async_init_plugin_report(cls) -> None:
"""异步初始化插件统计标记。"""
await cls._report_service().async_init_report(
enabled=settings.PLUGIN_STATISTIC_SHARE,
enabled=get_runtime_setting('PLUGIN_STATISTIC_SHARE'),
state_key=SystemConfigKey.PluginInstallReport,
reporter=cls.async_install_plugin_report,
)
@@ -514,7 +513,7 @@ class MoviePilotServerHelper:
"""
获取插件安装统计。
"""
if not settings.PLUGIN_STATISTIC_SHARE:
if not get_runtime_setting('PLUGIN_STATISTIC_SHARE'):
return {}
res = cls.plugin_statistic()
if res is not None and res.status_code == 200:
@@ -526,7 +525,7 @@ class MoviePilotServerHelper:
"""
异步获取插件安装统计。
"""
if not settings.PLUGIN_STATISTIC_SHARE:
if not get_runtime_setting('PLUGIN_STATISTIC_SHARE'):
return {}
res = await cls.async_plugin_statistic()
if res is not None and res.status_code == 200:
@@ -590,7 +589,7 @@ class MoviePilotServerHelper:
"""
上报单个插件安装统计。
"""
if not settings.PLUGIN_STATISTIC_SHARE:
if not get_runtime_setting('PLUGIN_STATISTIC_SHARE'):
return False
if not plugin_id:
return False
@@ -605,7 +604,7 @@ class MoviePilotServerHelper:
"""
异步上报单个插件安装统计。
"""
if not settings.PLUGIN_STATISTIC_SHARE:
if not get_runtime_setting('PLUGIN_STATISTIC_SHARE'):
return False
if not plugin_id:
return False
@@ -621,7 +620,7 @@ class MoviePilotServerHelper:
批量上报存量插件安装统计。
"""
return cls._report_service().report_plugins(
enabled=settings.PLUGIN_STATISTIC_SHARE,
enabled=get_runtime_setting('PLUGIN_STATISTIC_SHARE'),
items=items,
)
@@ -631,7 +630,7 @@ class MoviePilotServerHelper:
异步批量上报存量插件安装统计。
"""
return await cls._report_service().async_report_plugins(
enabled=settings.PLUGIN_STATISTIC_SHARE,
enabled=get_runtime_setting('PLUGIN_STATISTIC_SHARE'),
items=items,
)
@@ -820,7 +819,7 @@ class MoviePilotServerHelper:
"""
获取订阅统计数据。
"""
if not settings.SUBSCRIBE_STATISTIC_SHARE:
if not get_runtime_setting('SUBSCRIBE_STATISTIC_SHARE'):
return []
params = cls._build_subscribe_query_params(
page=page,
@@ -848,7 +847,7 @@ class MoviePilotServerHelper:
"""
异步获取订阅统计数据。
"""
if not settings.SUBSCRIBE_STATISTIC_SHARE:
if not get_runtime_setting('SUBSCRIBE_STATISTIC_SHARE'):
return []
params = cls._build_subscribe_query_params(
page=page,
@@ -866,7 +865,7 @@ class MoviePilotServerHelper:
"""
新增订阅统计。
"""
if not settings.SUBSCRIBE_STATISTIC_SHARE:
if not get_runtime_setting('SUBSCRIBE_STATISTIC_SHARE'):
return False
payload = cls._build_subscribe_statistic_payload(sub)
if not payload:
@@ -879,7 +878,7 @@ class MoviePilotServerHelper:
"""
异步新增订阅统计。
"""
if not settings.SUBSCRIBE_STATISTIC_SHARE:
if not get_runtime_setting('SUBSCRIBE_STATISTIC_SHARE'):
return False
payload = cls._build_subscribe_statistic_payload(sub)
if not payload:
@@ -890,14 +889,14 @@ class MoviePilotServerHelper:
@classmethod
def sub_reg_durable(cls, sub: dict) -> bool:
"""同步上报新增统计;明确禁用时视为无需投递。"""
if not settings.SUBSCRIBE_STATISTIC_SHARE:
if not get_runtime_setting('SUBSCRIBE_STATISTIC_SHARE'):
return True
return cls.sub_reg(sub)
@classmethod
async def async_sub_reg_durable(cls, sub: dict) -> bool:
"""异步上报新增统计;明确禁用时视为无需投递。"""
if not settings.SUBSCRIBE_STATISTIC_SHARE:
if not get_runtime_setting('SUBSCRIBE_STATISTIC_SHARE'):
return True
return await cls.async_sub_reg(sub)
@@ -906,7 +905,7 @@ class MoviePilotServerHelper:
"""
完成订阅统计。
"""
if not settings.SUBSCRIBE_STATISTIC_SHARE:
if not get_runtime_setting('SUBSCRIBE_STATISTIC_SHARE'):
return False
payload = cls._build_subscribe_statistic_payload(sub)
if not payload:
@@ -917,7 +916,7 @@ class MoviePilotServerHelper:
@classmethod
async def async_sub_done(cls, sub: dict) -> bool:
"""异步完成订阅统计,并仅在服务端确认成功时返回 True。"""
if not settings.SUBSCRIBE_STATISTIC_SHARE:
if not get_runtime_setting('SUBSCRIBE_STATISTIC_SHARE'):
return False
payload = cls._build_subscribe_statistic_payload(sub)
if not payload:
@@ -928,14 +927,14 @@ class MoviePilotServerHelper:
@classmethod
def sub_done_durable(cls, sub: dict) -> bool:
"""同步上报完成统计;明确禁用时视为无需投递。"""
if not settings.SUBSCRIBE_STATISTIC_SHARE:
if not get_runtime_setting('SUBSCRIBE_STATISTIC_SHARE'):
return True
return cls.sub_done(sub)
@classmethod
async def async_sub_done_durable(cls, sub: dict) -> bool:
"""异步上报完成统计;明确禁用时视为无需投递。"""
if not settings.SUBSCRIBE_STATISTIC_SHARE:
if not get_runtime_setting('SUBSCRIBE_STATISTIC_SHARE'):
return True
return await cls.async_sub_done(sub)
@@ -989,14 +988,14 @@ class MoviePilotServerHelper:
上报存量订阅统计。
"""
return cls._report_service().report_subscribes(
enabled=settings.SUBSCRIBE_STATISTIC_SHARE,
enabled=get_runtime_setting('SUBSCRIBE_STATISTIC_SHARE'),
)
@classmethod
async def async_sub_report(cls) -> bool:
"""异步上报存量订阅统计。"""
return await cls._report_service().async_report_subscribes(
enabled=settings.SUBSCRIBE_STATISTIC_SHARE,
enabled=get_runtime_setting('SUBSCRIBE_STATISTIC_SHARE'),
)
@classmethod
@@ -1011,7 +1010,7 @@ class MoviePilotServerHelper:
分享订阅。
"""
return cls._sharing_service().share_subscribe(
enabled=settings.SUBSCRIBE_STATISTIC_SHARE,
enabled=get_runtime_setting('SUBSCRIBE_STATISTIC_SHARE'),
subscribe_id=subscribe_id,
share_title=share_title,
share_comment=share_comment,
@@ -1030,7 +1029,7 @@ class MoviePilotServerHelper:
异步分享订阅。
"""
return await cls._sharing_service().async_share_subscribe(
enabled=settings.SUBSCRIBE_STATISTIC_SHARE,
enabled=get_runtime_setting('SUBSCRIBE_STATISTIC_SHARE'),
subscribe_id=subscribe_id,
share_title=share_title,
share_comment=share_comment,
@@ -1056,7 +1055,7 @@ class MoviePilotServerHelper:
"""
删除订阅分享。
"""
if not settings.SUBSCRIBE_STATISTIC_SHARE:
if not get_runtime_setting('SUBSCRIBE_STATISTIC_SHARE'):
return False, "当前没有开启订阅数据共享功能"
return cls._handle_response(
cls.subscribe_share_delete(share_id, cls.get_user_uuid()),
@@ -1068,7 +1067,7 @@ class MoviePilotServerHelper:
"""
异步删除订阅分享。
"""
if not settings.SUBSCRIBE_STATISTIC_SHARE:
if not get_runtime_setting('SUBSCRIBE_STATISTIC_SHARE'):
return False, "当前没有开启订阅数据共享功能"
return cls._handle_response(
await cls.async_subscribe_share_delete(share_id, cls.get_user_uuid()),
@@ -1080,7 +1079,7 @@ class MoviePilotServerHelper:
"""
复用订阅分享。
"""
if not settings.SUBSCRIBE_STATISTIC_SHARE:
if not get_runtime_setting('SUBSCRIBE_STATISTIC_SHARE'):
return False, "当前没有开启订阅数据共享功能"
return cls._handle_response(cls.subscribe_fork(share_id))
@@ -1089,7 +1088,7 @@ class MoviePilotServerHelper:
"""
异步复用订阅分享。
"""
if not settings.SUBSCRIBE_STATISTIC_SHARE:
if not get_runtime_setting('SUBSCRIBE_STATISTIC_SHARE'):
return False, "当前没有开启订阅数据共享功能"
return cls._handle_response(await cls.async_subscribe_fork(share_id))
@@ -1108,7 +1107,7 @@ class MoviePilotServerHelper:
"""
获取订阅分享数据。
"""
if not settings.SUBSCRIBE_STATISTIC_SHARE:
if not get_runtime_setting('SUBSCRIBE_STATISTIC_SHARE'):
return []
params = cls._build_subscribe_query_params(
page=page,
@@ -1136,7 +1135,7 @@ class MoviePilotServerHelper:
"""
异步获取订阅分享数据。
"""
if not settings.SUBSCRIBE_STATISTIC_SHARE:
if not get_runtime_setting('SUBSCRIBE_STATISTIC_SHARE'):
return []
params = cls._build_subscribe_query_params(
page=page,
@@ -1155,7 +1154,7 @@ class MoviePilotServerHelper:
"""
获取订阅分享统计数据。
"""
if not settings.SUBSCRIBE_STATISTIC_SHARE:
if not get_runtime_setting('SUBSCRIBE_STATISTIC_SHARE'):
return []
return cls._handle_list_response(cls.subscribe_share_statistics())
@@ -1165,7 +1164,7 @@ class MoviePilotServerHelper:
"""
异步获取订阅分享统计数据。
"""
if not settings.SUBSCRIBE_STATISTIC_SHARE:
if not get_runtime_setting('SUBSCRIBE_STATISTIC_SHARE'):
return []
return cls._handle_list_response(await cls.async_subscribe_share_statistics())
@@ -1252,7 +1251,7 @@ class MoviePilotServerHelper:
分享工作流。
"""
return cls._sharing_service().share_workflow(
enabled=settings.WORKFLOW_STATISTIC_SHARE,
enabled=get_runtime_setting('WORKFLOW_STATISTIC_SHARE'),
workflow_id=workflow_id,
share_title=share_title,
share_comment=share_comment,
@@ -1271,7 +1270,7 @@ class MoviePilotServerHelper:
异步分享工作流。
"""
return await cls._sharing_service().async_share_workflow(
enabled=settings.WORKFLOW_STATISTIC_SHARE,
enabled=get_runtime_setting('WORKFLOW_STATISTIC_SHARE'),
workflow_id=workflow_id,
share_title=share_title,
share_comment=share_comment,
@@ -1283,7 +1282,7 @@ class MoviePilotServerHelper:
"""
删除工作流分享。
"""
if not settings.WORKFLOW_STATISTIC_SHARE:
if not get_runtime_setting('WORKFLOW_STATISTIC_SHARE'):
return False, "当前没有开启工作流数据共享功能"
return cls._handle_response(
cls.workflow_share_delete(share_id, cls.get_user_uuid()),
@@ -1295,7 +1294,7 @@ class MoviePilotServerHelper:
"""
异步删除工作流分享。
"""
if not settings.WORKFLOW_STATISTIC_SHARE:
if not get_runtime_setting('WORKFLOW_STATISTIC_SHARE'):
return False, "当前没有开启工作流数据共享功能"
return cls._handle_response(
await cls.async_workflow_share_delete(share_id, cls.get_user_uuid()),
@@ -1307,7 +1306,7 @@ class MoviePilotServerHelper:
"""
复用工作流分享。
"""
if not settings.WORKFLOW_STATISTIC_SHARE:
if not get_runtime_setting('WORKFLOW_STATISTIC_SHARE'):
return False, "当前没有开启工作流数据共享功能"
return cls._handle_response(cls.workflow_fork(share_id))
@@ -1316,7 +1315,7 @@ class MoviePilotServerHelper:
"""
异步复用工作流分享。
"""
if not settings.WORKFLOW_STATISTIC_SHARE:
if not get_runtime_setting('WORKFLOW_STATISTIC_SHARE'):
return False, "当前没有开启工作流数据共享功能"
return cls._handle_response(await cls.async_workflow_fork(share_id))
@@ -1331,7 +1330,7 @@ class MoviePilotServerHelper:
"""
获取工作流分享数据。
"""
if not settings.WORKFLOW_STATISTIC_SHARE:
if not get_runtime_setting('WORKFLOW_STATISTIC_SHARE'):
return []
return cls._handle_list_response(cls.workflow_shares({
"name": name,
@@ -1350,7 +1349,7 @@ class MoviePilotServerHelper:
"""
异步获取工作流分享数据。
"""
if not settings.WORKFLOW_STATISTIC_SHARE:
if not get_runtime_setting('WORKFLOW_STATISTIC_SHARE'):
return []
return cls._handle_list_response(await cls.async_workflow_shares({
"name": name,
@@ -1370,10 +1369,10 @@ class MoviePilotServerHelper:
"""
获取共享识别服务端地址。
"""
custom_api = (settings.MEDIA_RECOGNIZE_SHARE_API or "").strip()
custom_api = (get_runtime_setting('MEDIA_RECOGNIZE_SHARE_API') or "").strip()
if custom_api:
return custom_api.rstrip("/")
server_host = (settings.MP_SERVER_HOST or "").strip().rstrip("/")
server_host = (get_runtime_setting('MP_SERVER_HOST') or "").strip().rstrip("/")
if not server_host:
return None
return f"{server_host}{cls._RECOGNIZE_SHARE_PATH}"
@@ -1429,7 +1428,7 @@ class MoviePilotServerHelper:
"""
查询共享识别结果。
"""
if not settings.MEDIA_RECOGNIZE_SHARE:
if not get_runtime_setting('MEDIA_RECOGNIZE_SHARE'):
return None
params = cls._build_recognize_query_params(
meta=meta,
@@ -1453,7 +1452,7 @@ class MoviePilotServerHelper:
"""
异步查询共享识别结果。
"""
if not settings.MEDIA_RECOGNIZE_SHARE:
if not get_runtime_setting('MEDIA_RECOGNIZE_SHARE'):
return None
params = cls._build_recognize_query_params(
meta=meta,
@@ -1476,7 +1475,7 @@ class MoviePilotServerHelper:
"""
上报共享识别结果,电影、电视剧、音乐共用。
"""
if not settings.MEDIA_RECOGNIZE_SHARE:
if not get_runtime_setting('MEDIA_RECOGNIZE_SHARE'):
return False
payload = cls._build_recognize_report_payload(
meta=meta,
@@ -1498,7 +1497,7 @@ class MoviePilotServerHelper:
"""
异步上报共享识别结果,电影、电视剧、音乐共用。
"""
if not settings.MEDIA_RECOGNIZE_SHARE:
if not get_runtime_setting('MEDIA_RECOGNIZE_SHARE'):
return False
payload = cls._build_recognize_report_payload(
meta=meta,
@@ -1855,7 +1854,7 @@ class MoviePilotServerHelper:
"""
根据服务端基础地址和路径生成完整 URL。
"""
return f"{settings.MP_SERVER_HOST.rstrip('/')}{path}"
return f"{get_runtime_setting('MP_SERVER_HOST').rstrip('/')}{path}"
@classmethod
def _get(
@@ -1869,7 +1868,7 @@ class MoviePilotServerHelper:
发送服务端 GET 请求,默认携带安装用户 ID。
"""
return RequestUtils(
proxies=settings.PROXY,
proxies=get_runtime_setting('PROXY'),
timeout=timeout,
headers=cls.build_headers(url) if include_user_uid else {},
).get_res(url, params=params)
@@ -1886,7 +1885,7 @@ class MoviePilotServerHelper:
异步发送服务端 GET 请求,默认携带安装用户 ID。
"""
return await AsyncRequestUtils(
proxies=settings.PROXY,
proxies=get_runtime_setting('PROXY'),
timeout=timeout,
headers=cls.build_headers(url) if include_user_uid else {},
).get_res(url, params=params)
@@ -1897,7 +1896,7 @@ class MoviePilotServerHelper:
发送携带安装用户 ID 的服务端 JSON POST 请求。
"""
return RequestUtils(
proxies=settings.PROXY,
proxies=get_runtime_setting('PROXY'),
timeout=timeout,
headers=cls.build_headers(url, content_type="application/json"),
).post(url, json=payload)
@@ -1908,7 +1907,7 @@ class MoviePilotServerHelper:
异步发送携带安装用户 ID 的服务端 JSON POST 请求。
"""
return await AsyncRequestUtils(
proxies=settings.PROXY,
proxies=get_runtime_setting('PROXY'),
timeout=timeout,
headers=cls.build_headers(url, content_type="application/json"),
).post(url, json=payload)
@@ -1919,7 +1918,7 @@ class MoviePilotServerHelper:
发送携带安装用户 ID 的服务端 DELETE 请求。
"""
return RequestUtils(
proxies=settings.PROXY,
proxies=get_runtime_setting('PROXY'),
timeout=timeout,
headers=cls.build_headers(url),
).delete_res(url, params=params)
@@ -1930,7 +1929,7 @@ class MoviePilotServerHelper:
异步发送携带安装用户 ID 的服务端 DELETE 请求。
"""
return await AsyncRequestUtils(
proxies=settings.PROXY,
proxies=get_runtime_setting('PROXY'),
timeout=timeout,
headers=cls.build_headers(url),
).delete_res(url, params=params)
+14 -13
View File
@@ -9,7 +9,7 @@ from typing import Any, Callable, Optional, Protocol
from urllib.parse import urlparse
from app.runtime.log import logger
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
from app.runtime.managed_resources import (
acquire_managed_resource,
acquire_managed_resource_async,
@@ -17,10 +17,6 @@ from app.runtime.managed_resources import (
from app.adapters.network.http import RequestUtils, cookie_parse
# 保留旧插件可覆盖的模块级入口,默认通过 runtime 代理动态读取浏览器配置。
settings = RuntimeSettingsCompat()
class BrowserElement(Protocol):
"""
页面元素的最小接口,避免为了类型标注直接导入 Playwright。
@@ -721,8 +717,8 @@ class BrowserSessionHelper:
) -> BrowserContext:
"""按宿主反检测配置创建 CloakBrowser 上下文。"""
context_kwargs = {
"humanize": settings.CLOAKBROWSER_HUMANIZE,
"human_preset": settings.CLOAKBROWSER_HUMAN_PRESET,
"humanize": get_runtime_setting('CLOAKBROWSER_HUMANIZE'),
"human_preset": get_runtime_setting('CLOAKBROWSER_HUMAN_PRESET'),
}
if user_agent:
context_kwargs["user_agent"] = user_agent
@@ -922,14 +918,18 @@ class PlaywrightHelper:
"""
兼容旧的 PlaywrightHelper(browser_type=...) 构造方式。
"""
self.browser_type = browser_type or settings.PLAYWRIGHT_BROWSER_TYPE
self.browser_type = browser_type or get_runtime_setting(
"PLAYWRIGHT_BROWSER_TYPE"
)
@staticmethod
def __browser_emulation() -> str:
"""
当前浏览器仿真类型。
"""
return (settings.BROWSER_EMULATION or "cloakbrowser").lower()
return (
get_runtime_setting('BROWSER_EMULATION') or "cloakbrowser"
).lower()
@staticmethod
def __launch_cloakbrowser_context(headless: bool,
@@ -941,8 +941,8 @@ class PlaywrightHelper:
return launch_browser_context(headless=headless,
proxy=proxies,
user_agent=user_agent,
humanize=settings.CLOAKBROWSER_HUMANIZE,
human_preset=settings.CLOAKBROWSER_HUMAN_PRESET)
humanize=get_runtime_setting('CLOAKBROWSER_HUMANIZE'),
human_preset=get_runtime_setting('CLOAKBROWSER_HUMAN_PRESET'))
@staticmethod
def __fs_cookie_str(cookies: list) -> str:
@@ -960,11 +960,12 @@ class PlaywrightHelper:
调用 FlareSolverr 解决 Cloudflare 并返回 solution 结果
参考: https://github.com/FlareSolverr/FlareSolverr
"""
if not settings.FLARESOLVERR_URL:
flaresolverr_url = get_runtime_setting('FLARESOLVERR_URL')
if not flaresolverr_url:
logger.warn("未配置 FLARESOLVERR_URL,无法使用 FlareSolverr")
return None
fs_api = settings.FLARESOLVERR_URL.rstrip("/") + "/v1"
fs_api = flaresolverr_url.rstrip("/") + "/v1"
session_id = None
try:
+6 -6
View File
@@ -27,10 +27,8 @@ import threading
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Union
from app.runtime.settings import RuntimeSettingsCompat
settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.runtime.settings import get_runtime_setting
# worker 脚本路径。用绝对路径直接执行,而不是 -m 或 import:
# 直接执行文件不会触发 app/__init__.py 的导入链,代理启动才是毫秒级的
@@ -258,7 +256,7 @@ class FileSystemProxy:
"""
if self._timeout_override is not None:
return self._timeout_override
return float(getattr(settings, "FS_PROXY_TIMEOUT", DEFAULT_TIMEOUT))
return float(get_runtime_setting("FS_PROXY_TIMEOUT", DEFAULT_TIMEOUT))
@property
def _stall_timeout(self) -> float:
@@ -267,14 +265,16 @@ class FileSystemProxy:
"""
if self._stall_timeout_override is not None:
return self._stall_timeout_override
return float(getattr(settings, "FS_PROXY_STALL_TIMEOUT", DEFAULT_STALL_TIMEOUT))
return float(
get_runtime_setting("FS_PROXY_STALL_TIMEOUT", DEFAULT_STALL_TIMEOUT)
)
@staticmethod
def _enabled() -> bool:
"""
代理是否启用。关闭时退回直接调用,行为与引入代理之前完全一致。
"""
return bool(getattr(settings, "FS_PROXY_ENABLED", True))
return bool(get_runtime_setting("FS_PROXY_ENABLED", True))
@staticmethod
def _direct(op: str, payload: Dict[str, Any]) -> Any:
+1 -1
View File
@@ -52,7 +52,7 @@ class PluginDependencyInstaller:
self._helper = helper
self._installed_plugins_provider = installed_plugins_provider or (lambda: [])
self._plugin_dir = plugin_dir or (
Path(get_runtime_setting("ROOT_PATH")) / "app" / "plugins"
Path(get_runtime_setting('ROOT_PATH')) / "app" / "plugins"
)
@staticmethod
+11 -10
View File
@@ -16,10 +16,7 @@ from app.runtime.execution import (
run_in_threadpool_to_completion as _await_thread_operation,
)
from app.runtime.log import logger
from app.runtime.settings import RuntimeSettingsCompat
# 保留旧模块级入口,插件本地同步测试和旧扩展仍可能覆盖这些设置。
settings = RuntimeSettingsCompat()
from app.runtime.settings import get_runtime_setting
@dataclass(frozen=True, slots=True)
class PluginPackageCheckpoint:
@@ -58,7 +55,7 @@ class PluginPackageManager:
def __plugin_dir(plugin_id: str) -> Path:
"""解析插件运行目录并拒绝越出宿主插件根目录的标识。"""
plugins_root = (
Path(settings.ROOT_PATH) / "app" / "plugins"
Path(get_runtime_setting('ROOT_PATH')) / "app" / "plugins"
).resolve()
plugin_dir = (plugins_root / plugin_id.lower()).resolve()
if plugin_dir == plugins_root or not plugin_dir.is_relative_to(plugins_root):
@@ -74,7 +71,9 @@ class PluginPackageManager:
plugin_dir = self.__plugin_dir(plugin_id)
durable = transaction_id is not None
persistent_backup_dir = (
Path(settings.CONFIG_PATH) / "plugins_backup" / plugin_id.lower()
Path(get_runtime_setting('CONFIG_PATH'))
/ "plugins_backup"
/ plugin_id.lower()
).resolve()
backup_staging_dir = (
persistent_backup_dir.parent
@@ -89,9 +88,9 @@ class PluginPackageManager:
else None
)
transaction_root = (
Path(settings.CONFIG_PATH)
Path(get_runtime_setting('CONFIG_PATH'))
if durable
else Path(settings.TEMP_PATH)
else Path(get_runtime_setting('TEMP_PATH'))
)
transaction_dir = (
transaction_root
@@ -129,7 +128,9 @@ class PluginPackageManager:
"""按受控根目录和事务 ID 重建崩溃回放所需的文件引用。"""
plugin_dir = self.__plugin_dir(plugin_id)
persistent_backup_dir = (
Path(settings.CONFIG_PATH) / "plugins_backup" / plugin_id.lower()
Path(get_runtime_setting('CONFIG_PATH'))
/ "plugins_backup"
/ plugin_id.lower()
).resolve()
durable_backup = SystemUtils.is_docker()
return PluginPackageCheckpoint(
@@ -149,7 +150,7 @@ class PluginPackageManager:
else None
),
transaction_dir=(
Path(settings.CONFIG_PATH)
Path(get_runtime_setting('CONFIG_PATH'))
/ "plugin_transactions"
/ transaction_id
),
+11 -17
View File
@@ -5,18 +5,12 @@ import sysconfig
from pathlib import Path
from typing import Callable
from app.runtime import config as _runtime_config
from app.runtime.log import logger
from app.adapters.network.http import RequestUtils
from app.foundation.version import compare_version
from app.adapters.system.host import SystemUtils
from app.foundation.version import compare_version
from app.runtime.log import logger
from app.runtime.settings import get_runtime_setting
# 保留模块级旧 Settings 入口,旧插件和测试可能仍会对其做运行时覆盖;实现读取统一走 runtime 端口。
settings = _runtime_config.settings
ResourceVersionProvider = Callable[[], tuple[str, str]]
@@ -39,9 +33,9 @@ class ResourceHelper:
检测和更新资源包
"""
_base_dir: Path = get_runtime_setting("ROOT_PATH")
_base_dir: Path = get_runtime_setting('ROOT_PATH')
_resource_target = Path("app/application/site")
_version_flag = get_runtime_setting("RESOURCE_VERSION_FLAG")
_version_flag = get_runtime_setting('RESOURCE_VERSION_FLAG')
_repo = (
f"{get_runtime_setting('GITHUB_PROXY')}https://raw.githubusercontent.com/"
f"jxxghp/MoviePilot-Resources/main/package.{_version_flag}.json"
@@ -56,8 +50,8 @@ class ResourceHelper:
"""返回访问 GitHub 资源时应使用的代理配置。"""
return (
None
if get_runtime_setting("GITHUB_PROXY")
else get_runtime_setting("PROXY")
if get_runtime_setting('GITHUB_PROXY')
else get_runtime_setting('PROXY')
)
@staticmethod
@@ -97,7 +91,7 @@ class ResourceHelper:
"""读取 V3 资源清单。"""
response = RequestUtils(
proxies=self.proxies,
headers=get_runtime_setting("GITHUB_HEADERS"),
headers=get_runtime_setting('GITHUB_HEADERS'),
timeout=10,
).get_res(self._repo)
return response if response and response.status_code == 200 else None
@@ -115,7 +109,7 @@ class ResourceHelper:
:param indexer_version: 当前已加载的站点索引资源版本省略时使用组合根注入值
:return: 是否成功安装了需要由上层处理重启的新资源
"""
if not get_runtime_setting("AUTO_UPDATE_RESOURCE"):
if not get_runtime_setting('AUTO_UPDATE_RESOURCE'):
return False
if SystemUtils.is_frozen():
return False
@@ -170,8 +164,8 @@ class ResourceHelper:
if need_updates:
# 下载文件信息列表
r = RequestUtils(
proxies=get_runtime_setting("PROXY"),
headers=get_runtime_setting("GITHUB_HEADERS"),
proxies=get_runtime_setting('PROXY'),
headers=get_runtime_setting('GITHUB_HEADERS'),
timeout=30,
).get_res(self._files_api)
if r and not r.ok:
@@ -201,7 +195,7 @@ class ResourceHelper:
)
res = RequestUtils(
proxies=self.proxies,
headers=get_runtime_setting("GITHUB_HEADERS"),
headers=get_runtime_setting('GITHUB_HEADERS'),
timeout=180,
).get_res(download_url)
if not res:
+1 -1
View File
@@ -17,7 +17,7 @@ else:
def _rust_accel_enabled() -> bool:
"""读取 Rust 开关快照,组合根未装配时回退旧 Settings。"""
return bool(get_runtime_setting("RUST_ACCEL"))
return bool(get_runtime_setting('RUST_ACCEL'))
def is_required() -> bool:
+4 -4
View File
@@ -43,7 +43,7 @@ class SystemUpdateManager(metaclass=SingletonClass):
@property
def _root(self) -> Path:
return Path(get_runtime_setting("TEMP_PATH")) / "moviepilot-update"
return Path(get_runtime_setting('TEMP_PATH')) / "moviepilot-update"
@property
def _state_file(self) -> Path:
@@ -268,8 +268,8 @@ class SystemUpdateManager(metaclass=SingletonClass):
def _request(self) -> RequestUtils:
return RequestUtils(
proxies=get_runtime_setting("PROXY"),
headers=get_runtime_setting("GITHUB_HEADERS"),
proxies=get_runtime_setting('PROXY'),
headers=get_runtime_setting('GITHUB_HEADERS'),
timeout=60,
)
@@ -474,7 +474,7 @@ class SystemUpdateManager(metaclass=SingletonClass):
@staticmethod
def _proxied(url: str) -> str:
proxy = str(get_runtime_setting("GITHUB_PROXY") or "").strip()
proxy = str(get_runtime_setting('GITHUB_PROXY') or "").strip()
return f"{proxy}{url}" if proxy else url
+16 -13
View File
@@ -16,7 +16,7 @@ from fastapi.security import (
from app.runtime.cache import cached
from app.runtime.log import logger
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
from app.schemas.token import TokenPayload
SuperuserTokenPayloadProvider = Callable[[], TokenPayload]
@@ -28,15 +28,12 @@ _token_decoder: Optional[TokenDecoder] = None
JWT_ALGORITHM = "HS256"
# 兼容旧鉴权插件覆盖模块级设置;令牌实际策略仍由已注入 codec 和 runtime 配置提供。
settings = RuntimeSettingsCompat()
oauth2_scheme_manual_error = OAuth2PasswordBearer(
auto_error=False,
tokenUrl=f"{settings.API_V1_STR}/login/access-token",
tokenUrl=f"{get_runtime_setting('API_V1_STR')}/login/access-token",
)
resource_token_cookie = APIKeyCookie(
name=settings.PROJECT_NAME,
name=get_runtime_setting('PROJECT_NAME'),
auto_error=False,
scheme_name="resource_token_cookie",
)
@@ -133,12 +130,13 @@ def set_or_refresh_resource_token_cookie(
payload: TokenPayload,
) -> None:
"""复用匹配的资源令牌,或为当前身份写入新的安全 Cookie。"""
resource_token = request.cookies.get(settings.PROJECT_NAME)
project_name = get_runtime_setting('PROJECT_NAME')
resource_token = request.cookies.get(project_name)
if resource_token:
try:
decoded = jwt.decode(
resource_token,
settings.RESOURCE_SECRET_KEY,
get_runtime_setting('RESOURCE_SECRET_KEY'),
algorithms=[JWT_ALGORITHM],
)
exp = decoded.get("exp")
@@ -148,7 +146,12 @@ def set_or_refresh_resource_token_cookie(
tz=datetime.UTC,
) - datetime.datetime.now(datetime.UTC)
if remaining_time < timedelta(
seconds=settings.RESOURCE_ACCESS_TOKEN_EXPIRE_SECONDS / 3
seconds=(
get_runtime_setting(
"RESOURCE_ACCESS_TOKEN_EXPIRE_SECONDS"
)
/ 3
)
):
raise jwt.ExpiredSignatureError
expected_claims = {
@@ -177,7 +180,7 @@ def set_or_refresh_resource_token_cookie(
username=payload.username or "",
super_user=payload.super_user,
expires_delta=timedelta(
seconds=settings.RESOURCE_ACCESS_TOKEN_EXPIRE_SECONDS
seconds=get_runtime_setting('RESOURCE_ACCESS_TOKEN_EXPIRE_SECONDS')
),
level=payload.level,
purpose="resource",
@@ -187,7 +190,7 @@ def set_or_refresh_resource_token_cookie(
or request.headers.get("x-forwarded-proto", "").lower() == "https"
)
response.set_cookie(
key=settings.PROJECT_NAME,
key=project_name,
value=resource_token,
httponly=True,
secure=is_https,
@@ -261,11 +264,11 @@ def verify_apitoken(
token: Annotated[str | None, Security(_get_api_token)],
) -> str:
"""校验 URL 查询参数中的兼容 API Token。"""
return _verify_key(token, settings.API_TOKEN, "token")
return _verify_key(token, get_runtime_setting('API_TOKEN'), "token")
def verify_apikey(
apikey: Annotated[str | None, Security(_get_api_key)],
) -> str:
"""校验请求头或查询参数中的兼容 API Key。"""
return _verify_key(apikey, settings.API_TOKEN, "apikey")
return _verify_key(apikey, get_runtime_setting('API_TOKEN'), "apikey")
+3 -6
View File
@@ -17,10 +17,7 @@ from app.runtime.capabilities.model import (
SelectorSchema,
)
from app.runtime.capabilities.registry import CapabilityRegistry
from app.runtime.settings import RuntimeSettingsCompat
settings = RuntimeSettingsCompat()
from app.runtime.settings import get_runtime_setting, has_runtime_setting
_DEFAULT_CAPABILITY_ROOT = Path(__file__).resolve().parent
_SETTING_SELECTOR = "setting_truthy"
@@ -29,7 +26,7 @@ _SETTING_SELECTOR = "setting_truthy"
def _validate_setting_selector(config: Mapping[str, Any]) -> None:
"""限制 selector 只能读取已声明的应用设置。"""
key = config["key"]
if not isinstance(key, str) or not key or not hasattr(settings, key):
if not isinstance(key, str) or not key or not has_runtime_setting(key):
raise ValueError(f"未知应用设置:{key!r}")
@@ -221,4 +218,4 @@ def should_run_agent_service(spec: CapabilitySpec) -> bool:
or selector.kind != _SETTING_SELECTOR
):
raise ValueError(f"{spec.source}: 不是可协调的 Agent Service 声明")
return bool(getattr(settings, selector.config["key"]))
return bool(get_runtime_setting(selector.config["key"]))
+27 -28
View File
@@ -13,9 +13,8 @@ from typing import Any, Dict, Optional
from uuid import uuid4
from app.application.notification import get_notification_configs
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.adapters.network.http import RequestUtils
@@ -61,11 +60,11 @@ class OpenAIAudioProvider(AudioCapabilityProvider):
@staticmethod
def _input_credentials() -> tuple[Optional[str], Optional[str]]:
return settings.AUDIO_INPUT_API_KEY, settings.AUDIO_INPUT_BASE_URL
return get_runtime_setting('AUDIO_INPUT_API_KEY'), get_runtime_setting('AUDIO_INPUT_BASE_URL')
@staticmethod
def _output_credentials() -> tuple[Optional[str], Optional[str]]:
return settings.AUDIO_OUTPUT_API_KEY, settings.AUDIO_OUTPUT_BASE_URL
return get_runtime_setting('AUDIO_OUTPUT_API_KEY'), get_runtime_setting('AUDIO_OUTPUT_BASE_URL')
def is_available_for_audio_input(self) -> bool:
api_key, _ = self._input_credentials()
@@ -89,9 +88,9 @@ class OpenAIAudioProvider(AudioCapabilityProvider):
audio_file = BytesIO(content)
audio_file.name = filename
response = client.audio.transcriptions.create(
model=settings.AUDIO_INPUT_MODEL,
model=get_runtime_setting('AUDIO_INPUT_MODEL'),
file=audio_file,
language=settings.AUDIO_INPUT_LANGUAGE or "zh",
language=get_runtime_setting('AUDIO_INPUT_LANGUAGE') or "zh",
response_format="verbose_json",
)
text = getattr(response, "text", None)
@@ -109,12 +108,12 @@ class OpenAIAudioProvider(AudioCapabilityProvider):
if not api_key:
raise ValueError("音频输出 provider 未配置 API Key")
client = self._build_client(api_key=api_key, base_url=base_url)
voice_dir = settings.TEMP_PATH / "voice"
voice_dir = get_runtime_setting('TEMP_PATH') / "voice"
voice_dir.mkdir(parents=True, exist_ok=True)
output_path = voice_dir / f"{uuid4().hex}.opus"
response = client.audio.speech.create(
model=settings.AUDIO_OUTPUT_MODEL,
voice=settings.AUDIO_OUTPUT_VOICE,
model=get_runtime_setting('AUDIO_OUTPUT_MODEL'),
voice=get_runtime_setting('AUDIO_OUTPUT_VOICE'),
input=text,
response_format="opus",
)
@@ -163,22 +162,22 @@ class OpenAIChatAudioProvider(AudioCapabilityProvider):
@staticmethod
def _input_credentials() -> tuple[Optional[str], Optional[str]]:
return settings.AUDIO_INPUT_API_KEY, settings.AUDIO_INPUT_BASE_URL
return get_runtime_setting('AUDIO_INPUT_API_KEY'), get_runtime_setting('AUDIO_INPUT_BASE_URL')
@staticmethod
def _output_credentials() -> tuple[Optional[str], Optional[str]]:
return settings.AUDIO_OUTPUT_API_KEY, settings.AUDIO_OUTPUT_BASE_URL
return get_runtime_setting('AUDIO_OUTPUT_API_KEY'), get_runtime_setting('AUDIO_OUTPUT_BASE_URL')
def _normalize_stt_model(self) -> str:
return self._normalize_model(
model=settings.AUDIO_INPUT_MODEL,
model=get_runtime_setting('AUDIO_INPUT_MODEL'),
supported_models=self.SUPPORTED_STT_MODELS,
default_model=self.DEFAULT_STT_MODEL,
)
def _normalize_tts_model(self) -> str:
return self._normalize_model(
model=settings.AUDIO_OUTPUT_MODEL,
model=get_runtime_setting('AUDIO_OUTPUT_MODEL'),
supported_models=self.SUPPORTED_TTS_MODELS,
default_model=self.DEFAULT_TTS_MODEL,
)
@@ -268,7 +267,7 @@ class OpenAIChatAudioProvider(AudioCapabilityProvider):
return None
suffix = Path(filename or "").suffix.lower() or ".audio"
voice_dir = settings.TEMP_PATH / "voice"
voice_dir = get_runtime_setting('TEMP_PATH') / "voice"
voice_dir.mkdir(parents=True, exist_ok=True)
input_path = voice_dir / f"{uuid4().hex}{suffix}"
output_path = input_path.with_suffix(self.TRANSCODED_STT_SUFFIX)
@@ -391,7 +390,7 @@ class OpenAIChatAudioProvider(AudioCapabilityProvider):
if not normalized_audio:
return None
content, filename = normalized_audio
language = (settings.AUDIO_INPUT_LANGUAGE or "").strip()
language = (get_runtime_setting('AUDIO_INPUT_LANGUAGE') or "").strip()
prompt = "请将这段音频完整转写为文字,只输出转写结果,不要添加解释。"
if language:
prompt += f"音频主要语言是 {language}"
@@ -426,7 +425,7 @@ class OpenAIChatAudioProvider(AudioCapabilityProvider):
logger.error(
"%s TTS 当前不支持该模型或模型未配置: %s",
self.DISPLAY_NAME,
settings.AUDIO_OUTPUT_MODEL,
get_runtime_setting('AUDIO_OUTPUT_MODEL'),
)
return None
@@ -435,7 +434,7 @@ class OpenAIChatAudioProvider(AudioCapabilityProvider):
if not api_key:
raise ValueError("音频输出 provider 未配置 API Key")
client = self._build_client(api_key=api_key, base_url=base_url)
voice_dir = settings.TEMP_PATH / "voice"
voice_dir = get_runtime_setting('TEMP_PATH') / "voice"
voice_dir.mkdir(parents=True, exist_ok=True)
wav_path = voice_dir / f"{uuid4().hex}.wav"
request = {
@@ -448,7 +447,7 @@ class OpenAIChatAudioProvider(AudioCapabilityProvider):
],
"audio": {
"format": self.AUDIO_RESPONSE_FORMAT,
"voice": settings.AUDIO_OUTPUT_VOICE or self.DEFAULT_VOICE,
"voice": get_runtime_setting('AUDIO_OUTPUT_VOICE') or self.DEFAULT_VOICE,
},
}
if self.INCLUDE_AUDIO_MODALITIES:
@@ -487,7 +486,7 @@ class MiMoAudioProvider(OpenAIChatAudioProvider):
)
def _normalize_tts_model(self) -> str:
model = (settings.AUDIO_OUTPUT_MODEL or "").strip().lower()
model = (get_runtime_setting('AUDIO_OUTPUT_MODEL') or "").strip().lower()
if not model or not model.startswith("mimo-"):
return self.DEFAULT_TTS_MODEL
return model
@@ -546,21 +545,21 @@ class MiniMaxAudioProvider(OpenAIChatAudioProvider):
def _normalize_stt_model(self) -> str:
"""将非 MiniMax 的默认转写模型名兜底为 MiniMax 对话模型。"""
model = (settings.AUDIO_INPUT_MODEL or "").strip()
model = (get_runtime_setting('AUDIO_INPUT_MODEL') or "").strip()
if not model or model.lower().startswith(("gpt-", "mimo-")):
return self.DEFAULT_STT_MODEL
return model
def _normalize_tts_model(self) -> str:
"""将非 MiniMax 语音模型兜底为官方 T2A 模型。"""
model = (settings.AUDIO_OUTPUT_MODEL or "").strip().lower()
model = (get_runtime_setting('AUDIO_OUTPUT_MODEL') or "").strip().lower()
if model in self.SUPPORTED_TTS_MODELS:
return model
return self.DEFAULT_TTS_MODEL
def _normalize_voice_id(self) -> str:
"""将其他 provider 的默认音色兜底为 MiniMax 中文系统音色。"""
voice_id = (settings.AUDIO_OUTPUT_VOICE or "").strip()
voice_id = (get_runtime_setting('AUDIO_OUTPUT_VOICE') or "").strip()
if not voice_id or voice_id in {"alloy", "mimo_default"}:
return self.DEFAULT_VOICE
return voice_id
@@ -599,7 +598,7 @@ class MiniMaxAudioProvider(OpenAIChatAudioProvider):
"Content-Type": "application/json",
"Accept": "application/json",
},
proxies=settings.PROXY or {},
proxies=get_runtime_setting('PROXY') or {},
timeout=60,
).post_res(
url=self._build_t2a_url(base_url),
@@ -637,7 +636,7 @@ class MiniMaxAudioProvider(OpenAIChatAudioProvider):
if not audio_data:
raise ValueError("MiniMax T2A 响应中没有音频数据")
voice_dir = settings.TEMP_PATH / "voice"
voice_dir = get_runtime_setting('TEMP_PATH') / "voice"
voice_dir.mkdir(parents=True, exist_ok=True)
output_path = voice_dir / f"{uuid4().hex}.opus"
output_path.write_bytes(self._decode_audio_payload(audio_data))
@@ -681,9 +680,9 @@ class AgentCapabilityManager:
@classmethod
def get_audio_provider(cls, mode: str) -> Optional[AudioCapabilityProvider]:
provider_name = cls._normalize_provider_name(
settings.AUDIO_INPUT_PROVIDER
get_runtime_setting('AUDIO_INPUT_PROVIDER')
if (mode or "").lower() == "input"
else settings.AUDIO_OUTPUT_PROVIDER
else get_runtime_setting('AUDIO_OUTPUT_PROVIDER')
)
provider = cls._audio_providers.get(provider_name)
if provider:
@@ -701,12 +700,12 @@ class AgentCapabilityManager:
@staticmethod
def supports_audio_input() -> bool:
"""当前 Agent 是否启用音频输入能力。"""
return bool(settings.LLM_SUPPORT_AUDIO_INPUT)
return bool(get_runtime_setting('LLM_SUPPORT_AUDIO_INPUT'))
@staticmethod
def supports_audio_output() -> bool:
"""当前 Agent 是否启用音频输出能力。"""
return bool(settings.LLM_SUPPORT_AUDIO_OUTPUT)
return bool(get_runtime_setting('LLM_SUPPORT_AUDIO_OUTPUT'))
@classmethod
def is_audio_input_available(cls) -> bool:
+24 -26
View File
@@ -11,10 +11,8 @@ from urllib.parse import urlsplit
from langchain_core.messages import AIMessage, AIMessageChunk
from app.agent.llm.gateway import resolve_llm_provider_runtime
from app.runtime.settings import RuntimeSettingsCompat
settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.runtime.settings import get_runtime_setting
if TYPE_CHECKING:
from app.agent.llm.server_tools import ServerToolResolution
@@ -148,8 +146,8 @@ def _resolve_llm_proxy(use_proxy: bool | None = None) -> str | None:
"""
解析本次 LLM 调用应使用的系统代理地址
"""
should_use_proxy = settings.LLM_USE_PROXY if use_proxy is None else use_proxy
return settings.PROXY_HOST if should_use_proxy and settings.PROXY_HOST else None
should_use_proxy = get_runtime_setting('LLM_USE_PROXY') if use_proxy is None else use_proxy
return get_runtime_setting('PROXY_HOST') if should_use_proxy and get_runtime_setting('PROXY_HOST') else None
def _build_httpx_proxy_kwargs(proxy_url: str | None) -> dict[str, str]:
@@ -539,7 +537,7 @@ class LLMHelper:
record_input = cls._source_input_limit(model_record)
metadata_input = cls._source_input_limit(metadata_source)
profile_input = cls._positive_token_limit(profile.get("max_input_tokens"))
configured_k = cls._positive_token_limit(settings.LLM_MAX_CONTEXT_TOKENS)
configured_k = cls._positive_token_limit(get_runtime_setting('LLM_MAX_CONTEXT_TOKENS'))
configured_input = configured_k * 1000 if configured_k else None
endpoint_matched = runtime.get("model_profile_endpoint_matched") is True
@@ -790,8 +788,8 @@ class LLMHelper:
base_url_preset: Optional[str] = None,
) -> Optional[bool]:
"""复用 provider 目录缓存解析当前模型是否支持图片输入。"""
provider_name = str(provider if provider is not None else settings.LLM_PROVIDER).strip()
model_name = str(model if model is not None else settings.LLM_MODEL).strip()
provider_name = str(provider if provider is not None else get_runtime_setting('LLM_PROVIDER')).strip()
model_name = str(model if model is not None else get_runtime_setting('LLM_MODEL')).strip()
if not provider_name or not model_name:
return None
@@ -799,11 +797,11 @@ class LLMHelper:
metadata = resolve_llm_provider_runtime().resolve_cached_model_metadata(
provider_id=provider_name,
model_id=model_name,
base_url=base_url if base_url is not None else settings.LLM_BASE_URL,
base_url=base_url if base_url is not None else get_runtime_setting('LLM_BASE_URL'),
base_url_preset_id=(
base_url_preset
if base_url_preset is not None
else settings.LLM_BASE_URL_PRESET
else get_runtime_setting('LLM_BASE_URL_PRESET')
),
)
except Exception as err:
@@ -828,7 +826,7 @@ class LLMHelper:
被兼容端点以 400 拒绝无参调用保持旧版只读总开关语义
未知自定义模型也保持原有开关语义
"""
if not settings.LLM_SUPPORT_IMAGE_INPUT:
if not get_runtime_setting('LLM_SUPPORT_IMAGE_INPUT'):
return False
if provider is None and model is None:
return True
@@ -857,8 +855,8 @@ class LLMHelper:
这主要用于单测 stub 环境以及极端的最小运行环境正常生产路径仍优先
`LLMProviderManager.resolve_runtime()`
"""
api_key_value = api_key if api_key is not None else settings.LLM_API_KEY
base_url_value = base_url if base_url is not None else settings.LLM_BASE_URL
api_key_value = api_key if api_key is not None else get_runtime_setting('LLM_API_KEY')
base_url_value = base_url if base_url is not None else get_runtime_setting('LLM_BASE_URL')
if not api_key_value:
raise ValueError("未配置LLM API Key")
@@ -1037,7 +1035,7 @@ class LLMHelper:
"""
规范化 API 协议配置未知值统一回退为 ``auto`` 以保持兼容
"""
normalized = str(api_protocol or settings.LLM_API_PROTOCOL or "").strip().lower()
normalized = str(api_protocol or get_runtime_setting('LLM_API_PROTOCOL') or "").strip().lower()
if normalized in {"auto", "chat_completions", "responses"}:
return normalized
if normalized:
@@ -1185,15 +1183,15 @@ class LLMHelper:
:param prompt_cache_key: 同一 Agent 会话内稳定且脱敏的提示词缓存路由键
:return: LLM实例
"""
provider_name = str(provider if provider is not None else settings.LLM_PROVIDER).lower()
model_name = model if model is not None else settings.LLM_MODEL
api_key_value = api_key if api_key is not None else settings.LLM_API_KEY
base_url_value = base_url if base_url is not None else settings.LLM_BASE_URL
provider_name = str(provider if provider is not None else get_runtime_setting('LLM_PROVIDER')).lower()
model_name = model if model is not None else get_runtime_setting('LLM_MODEL')
api_key_value = api_key if api_key is not None else get_runtime_setting('LLM_API_KEY')
base_url_value = base_url if base_url is not None else get_runtime_setting('LLM_BASE_URL')
base_url_preset_value = (
base_url_preset if base_url_preset is not None else settings.LLM_BASE_URL_PRESET
base_url_preset if base_url_preset is not None else get_runtime_setting('LLM_BASE_URL_PRESET')
)
user_agent_value = user_agent if user_agent is not None else settings.LLM_USER_AGENT
temperature_value = temperature if temperature is not None else settings.LLM_TEMPERATURE
user_agent_value = user_agent if user_agent is not None else get_runtime_setting('LLM_USER_AGENT')
temperature_value = temperature if temperature is not None else get_runtime_setting('LLM_TEMPERATURE')
normalized_thinking_level = cls._resolve_thinking_level(
thinking_level=thinking_level,
)
@@ -1228,12 +1226,12 @@ class LLMHelper:
mode=(
web_search_mode
if web_search_mode is not None
else getattr(settings, "LLM_WEB_SEARCH_MODE", "local")
else get_runtime_setting("LLM_WEB_SEARCH_MODE", "local")
),
api_protocol=(
api_protocol
if api_protocol is not None
else settings.LLM_API_PROTOCOL
else get_runtime_setting('LLM_API_PROTOCOL')
),
base_url=runtime.get("base_url"),
)
@@ -1345,7 +1343,7 @@ class LLMHelper:
credentials=aws_auth,
base_url=runtime.get("base_url"),
use_proxy=use_proxy,
read_timeout=settings.LLM_TOOL_TIMEOUT,
read_timeout=get_runtime_setting('LLM_TOOL_TIMEOUT'),
)
model = bedrock_model_cls(
model_id=model_name,
@@ -1492,8 +1490,8 @@ class LLMHelper:
:param api_protocol: OpenAI 兼容接口 API 协议未显式传入时沿用已保存配置
:param web_search_mode: 联网搜索模式未显式传入时沿用已保存配置
"""
provider_name = provider if provider is not None else settings.LLM_PROVIDER
model_name = model if model is not None else settings.LLM_MODEL
provider_name = provider if provider is not None else get_runtime_setting('LLM_PROVIDER')
model_name = model if model is not None else get_runtime_setting('LLM_MODEL')
start = time.perf_counter()
llm_kwargs = {
"streaming": False,
+21 -22
View File
@@ -20,9 +20,8 @@ import aiofiles
import httpx
import jwt
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.application.configuration import get_configured_system_config
from app.runtime.log import logger
from app.schemas.types import LlmProviderAction, SystemConfigKey
@@ -268,7 +267,7 @@ class LLMProviderManager(metaclass=Singleton):
self._models_dev_data: dict[str, Any] | None = None
self._models_dev_loaded_at: float = 0
self._models_dev_cache_path = (
Path(settings.TEMP_PATH) / "llm_provider_models_dev_cache.json"
Path(get_runtime_setting('TEMP_PATH')) / "llm_provider_models_dev_cache.json"
)
def _cleanup_auth_sessions_locked(self, now: Optional[float] = None) -> None:
@@ -1498,13 +1497,13 @@ class LLMProviderManager(metaclass=Singleton):
def _build_httpx_kwargs(self, use_proxy: Optional[bool] = None) -> dict[str, Any]:
"""构造用于 httpx 客户端的参数,如代理等。"""
should_use_proxy = settings.LLM_USE_PROXY if use_proxy is None else use_proxy
should_use_proxy = get_runtime_setting('LLM_USE_PROXY') if use_proxy is None else use_proxy
kwargs: dict[str, Any] = {
"timeout": self._DEFAULT_TIMEOUT,
"trust_env": False,
}
if should_use_proxy and settings.PROXY_HOST:
kwargs[self._httpx_proxy_key()] = settings.PROXY_HOST
if should_use_proxy and get_runtime_setting('PROXY_HOST'):
kwargs[self._httpx_proxy_key()] = get_runtime_setting('PROXY_HOST')
return kwargs
@staticmethod
@@ -1616,7 +1615,7 @@ class LLMProviderManager(metaclass=Singleton):
async def _fetch_models_dev(self, use_proxy: Optional[bool] = None) -> dict[str, Any]:
"""通过网络请求获取最新 models.dev 数据。"""
headers = {"User-Agent": settings.USER_AGENT}
headers = {"User-Agent": get_runtime_setting('USER_AGENT')}
async with httpx.AsyncClient(**self._build_httpx_kwargs(use_proxy)) as client:
response = await client.get(self._MODELS_DEV_URL, headers=headers)
response.raise_for_status()
@@ -2043,10 +2042,10 @@ class LLMProviderManager(metaclass=Singleton):
from google import genai
from google.genai.types import HttpOptions
should_use_proxy = settings.LLM_USE_PROXY if use_proxy is None else use_proxy
should_use_proxy = get_runtime_setting('LLM_USE_PROXY') if use_proxy is None else use_proxy
client_args: dict[str, Any] = {"trust_env": False}
if should_use_proxy and settings.PROXY_HOST:
client_args[self._httpx_proxy_key()] = settings.PROXY_HOST
if should_use_proxy and get_runtime_setting('PROXY_HOST'):
client_args[self._httpx_proxy_key()] = get_runtime_setting('PROXY_HOST')
http_options = HttpOptions(
client_args=client_args,
async_client_args=client_args,
@@ -2160,10 +2159,10 @@ class LLMProviderManager(metaclass=Singleton):
"""
from botocore.config import Config
should_use_proxy = settings.LLM_USE_PROXY if use_proxy is None else use_proxy
should_use_proxy = get_runtime_setting('LLM_USE_PROXY') if use_proxy is None else use_proxy
proxies = None
if should_use_proxy and settings.PROXY_HOST:
proxies = {"http": settings.PROXY_HOST, "https": settings.PROXY_HOST}
if should_use_proxy and get_runtime_setting('PROXY_HOST'):
proxies = {"http": get_runtime_setting('PROXY_HOST'), "https": get_runtime_setting('PROXY_HOST')}
return Config(
connect_timeout=10,
read_timeout=60,
@@ -2388,7 +2387,7 @@ class LLMProviderManager(metaclass=Singleton):
仅补充 Copilot 必需的意图头避免重复覆盖
"""
headers = {
"User-Agent": settings.USER_AGENT,
"User-Agent": get_runtime_setting('USER_AGENT'),
"Openai-Intent": "conversation-edits",
"x-initiator": "user",
}
@@ -2769,7 +2768,7 @@ class LLMProviderManager(metaclass=Singleton):
f"{self._CHATGPT_ISSUER}/api/accounts/deviceauth/usercode",
headers={
"Content-Type": "application/json",
"User-Agent": settings.USER_AGENT,
"User-Agent": get_runtime_setting('USER_AGENT'),
},
json={"client_id": self._CHATGPT_CLIENT_ID},
)
@@ -2806,7 +2805,7 @@ class LLMProviderManager(metaclass=Singleton):
headers={
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": settings.USER_AGENT,
"User-Agent": get_runtime_setting('USER_AGENT'),
},
json={
"client_id": self._COPILOT_CLIENT_ID,
@@ -3036,11 +3035,11 @@ class LLMProviderManager(metaclass=Singleton):
"""管理动作:使用传入配置或当前已保存配置执行一次最小 LLM 调用。"""
from app.agent.llm.helper import LLMHelper, LLMTestTimeout
provider_name = provider or settings.LLM_PROVIDER
model = params.get("model") if params.get("model") is not None else settings.LLM_MODEL
provider_name = provider or get_runtime_setting('LLM_PROVIDER')
model = params.get("model") if params.get("model") is not None else get_runtime_setting('LLM_MODEL')
enabled = params.get("enabled")
enabled = bool(enabled) if enabled is not None else bool(settings.AI_AGENT_ENABLE)
api_key = params.get("api_key") if params.get("api_key") is not None else settings.LLM_API_KEY
enabled = bool(enabled) if enabled is not None else bool(get_runtime_setting('AI_AGENT_ENABLE'))
api_key = params.get("api_key") if params.get("api_key") is not None else get_runtime_setting('LLM_API_KEY')
data = {"provider": provider_name, "model": model}
if not provider_name:
@@ -3151,7 +3150,7 @@ class LLMProviderManager(metaclass=Singleton):
f"{self._CHATGPT_ISSUER}/api/accounts/deviceauth/token",
headers={
"Content-Type": "application/json",
"User-Agent": settings.USER_AGENT,
"User-Agent": get_runtime_setting('USER_AGENT'),
},
json={
"device_auth_id": session.context["device_auth_id"],
@@ -3196,7 +3195,7 @@ class LLMProviderManager(metaclass=Singleton):
headers={
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": settings.USER_AGENT,
"User-Agent": get_runtime_setting('USER_AGENT'),
},
json={
"client_id": self._COPILOT_CLIENT_ID,
+2 -4
View File
@@ -6,15 +6,13 @@ from typing import Dict, List, Optional
from langchain_core.messages import BaseMessage, messages_from_dict, messages_to_dict
from app.runtime.settings import RuntimeSettingsCompat
settings = RuntimeSettingsCompat()
from app.application.agentdata import get_agent_chat_port
from app.application.messaging.chat import (
get_configured_agent_chat_persistence,
get_configured_agent_chat_service,
)
from app.runtime.log import logger
from app.runtime.settings import get_runtime_setting
from app.schemas.agent import ConversationMemory
@@ -228,7 +226,7 @@ class MemoryManager:
for cache_key, memory in self.memory_cache.items():
if (
current_time - memory.updated_at
).days > settings.LLM_MEMORY_RETENTION_DAYS:
).days > get_runtime_setting('LLM_MEMORY_RETENTION_DAYS'):
expired_sessions.append(cache_key)
# 只清理内存缓存,不删除Redis中的键(Redis会自动过期)
+48 -49
View File
@@ -68,9 +68,8 @@ from app.agent.tools.impl.mcp import (
)
from app.agent.tools.impl.query_system_settings import QuerySystemSettingsTool
from app.chain.agent import AgentChain
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.runtime.events import eventmanager
from app.runtime.observability import record_metric
from app.application.plugin.runtime import get_plugin_manager
@@ -622,7 +621,7 @@ class MoviePilotAgent:
def _get_recursion_limit() -> int:
"""读取 LangGraph 递归上限,防止模型持续循环调用工具。"""
try:
limit = int(settings.LLM_MAX_ITERATIONS or 0)
limit = int(get_runtime_setting('LLM_MAX_ITERATIONS') or 0)
except (TypeError, ValueError):
limit = 0
return limit if limit > 0 else 128
@@ -747,7 +746,7 @@ class MoviePilotAgent:
self._session_usage.cache_usage_available |= cache_usage_available
provider_type = _agent_provider_metric_type(
(self._llm_provider_selection or {}).get("provider")
or settings.LLM_PROVIDER
or get_runtime_setting('LLM_PROVIDER')
)
if input_tokens:
record_metric(
@@ -872,14 +871,14 @@ class MoviePilotAgent:
not self._session_usage.model
and self._session_usage.last_request_sequence == 0
):
self._session_usage.model = settings.LLM_MODEL
self._session_usage.model = get_runtime_setting('LLM_MODEL')
if (
not self._session_usage.context_window_tokens
and self._session_usage.last_request_sequence == 0
):
self._session_usage.context_window_tokens = (
settings.LLM_MAX_CONTEXT_TOKENS * 1000
if settings.LLM_MAX_CONTEXT_TOKENS
get_runtime_setting('LLM_MAX_CONTEXT_TOKENS') * 1000
if get_runtime_setting('LLM_MAX_CONTEXT_TOKENS')
else None
)
return self._session_usage.to_dict(self.session_id)
@@ -899,9 +898,9 @@ class MoviePilotAgent:
session_id=self.session_id,
selected_provider_id=selection.get("selected_provider_id"),
selected_provider_name=selection.get("selected_provider_name"),
provider=selection.get("provider") or settings.LLM_PROVIDER,
base_url=selection.get("base_url") or settings.LLM_BASE_URL,
model=self._session_usage.model or selection.get("model") or settings.LLM_MODEL,
provider=selection.get("provider") or get_runtime_setting('LLM_PROVIDER'),
base_url=selection.get("base_url") or get_runtime_setting('LLM_BASE_URL'),
model=self._session_usage.model or selection.get("model") or get_runtime_setting('LLM_MODEL'),
input_tokens=self._session_usage.total_input_tokens,
output_tokens=self._session_usage.total_output_tokens,
total_tokens=self._session_usage.total_tokens,
@@ -1272,7 +1271,7 @@ class MoviePilotAgent:
if self.is_background:
return False
# 啰嗦模式下始终需要流式输出来捕获工具调用前的 Agent 文字
if settings.AI_AGENT_VERBOSE:
if get_runtime_setting('AI_AGENT_VERBOSE'):
return True
try:
channel_enum = NotificationChannel(self.channel)
@@ -1321,16 +1320,16 @@ class MoviePilotAgent:
return self._llm_runtime_config
event_data = AgentLLMProviderEventData(
provider=settings.LLM_PROVIDER,
model=settings.LLM_MODEL,
api_key=settings.LLM_API_KEY,
base_url=settings.LLM_BASE_URL,
base_url_preset=settings.LLM_BASE_URL_PRESET,
user_agent=settings.LLM_USER_AGENT,
use_proxy=settings.LLM_USE_PROXY,
thinking_level=settings.LLM_THINKING_LEVEL,
api_protocol=settings.LLM_API_PROTOCOL,
web_search_mode=settings.LLM_WEB_SEARCH_MODE,
provider=get_runtime_setting('LLM_PROVIDER'),
model=get_runtime_setting('LLM_MODEL'),
api_key=get_runtime_setting('LLM_API_KEY'),
base_url=get_runtime_setting('LLM_BASE_URL'),
base_url_preset=get_runtime_setting('LLM_BASE_URL_PRESET'),
user_agent=get_runtime_setting('LLM_USER_AGENT'),
use_proxy=get_runtime_setting('LLM_USE_PROXY'),
thinking_level=get_runtime_setting('LLM_THINKING_LEVEL'),
api_protocol=get_runtime_setting('LLM_API_PROTOCOL'),
web_search_mode=get_runtime_setting('LLM_WEB_SEARCH_MODE'),
)
selected_event = await eventmanager.async_send_event(
ChainEventType.AgentLLMProvider,
@@ -1340,43 +1339,43 @@ class MoviePilotAgent:
provider = (
self._clean_optional_text(self._get_event_value(resolved_data, "provider"))
or settings.LLM_PROVIDER
or get_runtime_setting('LLM_PROVIDER')
)
model = (
self._clean_optional_text(self._get_event_value(resolved_data, "model"))
or settings.LLM_MODEL
or get_runtime_setting('LLM_MODEL')
)
api_key = (
self._clean_optional_text(self._get_event_value(resolved_data, "api_key"))
or settings.LLM_API_KEY
or get_runtime_setting('LLM_API_KEY')
)
base_url = (
self._clean_optional_text(self._get_event_value(resolved_data, "base_url"))
or settings.LLM_BASE_URL
or get_runtime_setting('LLM_BASE_URL')
)
base_url_preset = (
self._clean_optional_text(self._get_event_value(resolved_data, "base_url_preset"))
or settings.LLM_BASE_URL_PRESET
or get_runtime_setting('LLM_BASE_URL_PRESET')
)
user_agent = (
self._clean_optional_text(self._get_event_value(resolved_data, "user_agent"))
or settings.LLM_USER_AGENT
or get_runtime_setting('LLM_USER_AGENT')
)
use_proxy = self._get_event_value(resolved_data, "use_proxy")
if use_proxy is None:
use_proxy = settings.LLM_USE_PROXY
use_proxy = get_runtime_setting('LLM_USE_PROXY')
thinking_level = (
self._clean_optional_text(
self._get_event_value(resolved_data, "thinking_level")
)
or settings.LLM_THINKING_LEVEL
or get_runtime_setting('LLM_THINKING_LEVEL')
)
api_protocol = self._clean_optional_text(
self._get_event_value(resolved_data, "api_protocol")
) or settings.LLM_API_PROTOCOL
) or get_runtime_setting('LLM_API_PROTOCOL')
web_search_mode = self._clean_optional_text(
self._get_event_value(resolved_data, "web_search_mode")
) or settings.LLM_WEB_SEARCH_MODE
) or get_runtime_setting('LLM_WEB_SEARCH_MODE')
selected_provider_id = self._clean_optional_text(
self._get_event_value(resolved_data, "selected_provider_id")
)
@@ -1519,8 +1518,8 @@ class MoviePilotAgent:
清理执行错误中的密钥和尾部长说明避免把敏感字段或 SDK 调参文档直接发给用户
"""
sanitized = re.sub(r"\s+", " ", str(message or "")).strip()
if settings.LLM_API_KEY:
sanitized = sanitized.replace(settings.LLM_API_KEY, "***")
if get_runtime_setting('LLM_API_KEY'):
sanitized = sanitized.replace(get_runtime_setting('LLM_API_KEY'), "***")
sanitized = re.sub(
r"(?i)(api[_-]?key\s*[:=]\s*)([^\s,;]+)",
r"\1***",
@@ -1713,11 +1712,11 @@ class MoviePilotAgent:
bool(self._tool_context.get("is_admin")),
self.has_message_context,
self.is_background,
settings.AI_AGENT_VERBOSE,
settings.LLM_TEMPERATURE,
settings.LLM_MAX_CONTEXT_TOKENS,
settings.LLM_MAX_TOOLS,
settings.LLM_MAX_ITERATIONS,
get_runtime_setting('AI_AGENT_VERBOSE'),
get_runtime_setting('LLM_TEMPERATURE'),
get_runtime_setting('LLM_MAX_CONTEXT_TOKENS'),
get_runtime_setting('LLM_MAX_TOOLS'),
get_runtime_setting('LLM_MAX_ITERATIONS'),
self._public_runtime_config_signature(runtime_config),
agent_runtime_manager.current_signature(),
agent_mcp_manager.config_signature(),
@@ -2004,7 +2003,7 @@ class MoviePilotAgent:
)
skills_middleware = SkillsMiddleware(
sources=[str(agent_runtime_manager.skills_dir)],
bundled_skills_dir=str(settings.ROOT_PATH / "skills"),
bundled_skills_dir=str(get_runtime_setting('ROOT_PATH') / "skills"),
stream_handler=self.stream_handler,
)
skill_tools = list(getattr(skills_middleware, "tools", []) or [])
@@ -2060,7 +2059,7 @@ class MoviePilotAgent:
temporary_subagent_middlewares = ()
logger.debug(f"复用会话内 Agent 图: session_id={self.session_id}")
return cached_agent
max_tools = settings.LLM_MAX_TOOLS
max_tools = get_runtime_setting('LLM_MAX_TOOLS')
from app.agent.runtime_loader import get_tool_factory
always_include_tools = (
@@ -2558,7 +2557,7 @@ class MoviePilotAgent:
"agent.provider.duration",
time.perf_counter() - metric_started_at,
provider_type=_agent_provider_metric_type(
selection.get("provider") or settings.LLM_PROVIDER
selection.get("provider") or get_runtime_setting('LLM_PROVIDER')
),
outcome="success" if execution_success else "error",
)
@@ -2588,7 +2587,7 @@ class MoviePilotAgent:
source=None if broadcast else self.source,
mtype=MessageType.Agent,
userid=None if broadcast else self.user_id,
username=self.username or (settings.SUPERUSER if broadcast else None),
username=self.username or (get_runtime_setting('SUPERUSER') if broadcast else None),
original_message_id=None if broadcast else self.original_message_id,
original_chat_id=None if broadcast else self.original_chat_id,
title=title,
@@ -2704,10 +2703,10 @@ class AgentManager:
status = agent.get_session_status()
else:
status = _SessionUsageSnapshot(
model=settings.LLM_MODEL,
model=get_runtime_setting('LLM_MODEL'),
context_window_tokens=(
settings.LLM_MAX_CONTEXT_TOKENS * 1000
if settings.LLM_MAX_CONTEXT_TOKENS
get_runtime_setting('LLM_MAX_CONTEXT_TOKENS') * 1000
if get_runtime_setting('LLM_MAX_CONTEXT_TOKENS')
else None
),
).to_dict(session_id)
@@ -3515,7 +3514,7 @@ class AgentManager:
message=message,
channel=None,
source=None,
username=settings.SUPERUSER,
username=get_runtime_setting('SUPERUSER'),
reply_mode=reply_mode,
output_callback=output_callback,
allow_message_tools=allow_message_tools,
@@ -3538,7 +3537,7 @@ class AgentManager:
:param trigger_source: 触发入口scheduled-自动调度manual-显式立即执行
:return: 执行是否成功及结果摘要
"""
if not settings.AI_AGENT_ENABLE:
if not get_runtime_setting('AI_AGENT_ENABLE'):
return False, "AI Agent 未启用"
accepting_before_claim = self._accepting_tasks
task_service = get_agent_task_execution_service()
@@ -3564,7 +3563,7 @@ class AgentManager:
)
success = True
result = ""
notification_username = run.username or settings.SUPERUSER
notification_username = run.username or get_runtime_setting('SUPERUSER')
try:
result = await self.process_message(
session_id=run.session_id,
@@ -3650,7 +3649,7 @@ class AgentManager:
message=heartbeat_message,
channel=None,
source=None,
username=settings.SUPERUSER,
username=get_runtime_setting('SUPERUSER'),
reply_mode=ReplyMode.CAPTURE_ONLY,
allow_message_tools=True,
)
+4 -5
View File
@@ -10,9 +10,8 @@ from typing import Any, Dict, Optional
import yaml
from app.agent.llm.capability import AgentCapabilityManager
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.schemas.notification import ChannelCapability
from app.schemas.notification import ChannelCapabilities
@@ -301,9 +300,9 @@ class PromptManager:
def _get_runtime_path_lines() -> list[str]:
"""返回基础系统提示词需要常驻注入的全局运行路径。"""
paths = {
"项目根目录": settings.ROOT_PATH,
"配置目录": settings.CONFIG_PATH,
"临时目录": settings.TEMP_PATH,
"项目根目录": get_runtime_setting('ROOT_PATH'),
"配置目录": get_runtime_setting('CONFIG_PATH'),
"临时目录": get_runtime_setting('TEMP_PATH'),
}
return [f" - {label}: `{path}`" for label, path in paths.items()]
+22 -22
View File
@@ -9,15 +9,15 @@ from pathlib import Path
from typing import Dict, List, Optional, Tuple
from urllib.parse import urlencode, urljoin, urlparse
from app.agent.skills.metadata import parse_skill_metadata
from app.runtime.cache import cached, fresh
from app.runtime.settings import RuntimeSettingsCompat
settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.adapters.network.http import RequestUtils
from app.agent.skills.metadata import parse_skill_metadata
from app.application.configuration import get_runtime_settings
from app.foundation.singleton import WeakSingleton
from app.foundation.url import UrlUtils
from app.runtime.cache import cached, fresh
from app.runtime.config import Settings
from app.runtime.log import logger
from app.runtime.settings import get_runtime_setting
_SOURCE_META_FILENAME = ".moviepilot-skill-source.json"
_DEFAULT_BRANCHES = ("main", "master")
@@ -77,30 +77,30 @@ class SkillHelper(metaclass=WeakSingleton):
"""
返回用户技能目录所有市场安装的技能都落在这里
"""
return settings.CONFIG_PATH / "agent" / "skills"
return get_runtime_setting('CONFIG_PATH') / "agent" / "skills"
@staticmethod
def get_bundled_skills_dir() -> Path:
"""
返回仓库内置技能目录
"""
return settings.ROOT_PATH / "skills"
return get_runtime_setting('ROOT_PATH') / "skills"
@staticmethod
def get_market_sources() -> List[str]:
"""
解析配置中的技能市场列表
"""
if not settings.SKILL_MARKET:
if not get_runtime_setting('SKILL_MARKET'):
return []
return [item.strip() for item in settings.SKILL_MARKET.split(",") if item.strip()]
return [item.strip() for item in get_runtime_setting('SKILL_MARKET').split(",") if item.strip()]
@staticmethod
def get_default_market_sources() -> List[str]:
"""
返回系统默认的技能市场列表用于区分内置源和用户追加源
"""
skill_market_field = type(settings).model_fields.get("SKILL_MARKET")
skill_market_field = Settings.model_fields.get("SKILL_MARKET")
default_value = skill_market_field.default if skill_market_field else None
if not default_value:
return []
@@ -199,10 +199,10 @@ class SkillHelper(metaclass=WeakSingleton):
@staticmethod
def _persist_market_sources(sources: List[str]) -> Tuple[bool, str]:
"""
将技能源列表写回配置文件同步更新内存中的 settings
将技能源列表写回配置服务让后续读取立即看到新值
"""
filtered_sources = [item.strip() for item in sources if item and item.strip()]
success, message = settings.update_setting(
success, message = get_runtime_settings().update(
key="SKILL_MARKET",
value=",".join(filtered_sources),
)
@@ -1093,8 +1093,8 @@ class SkillHelper(metaclass=WeakSingleton):
}
strategies = []
if settings.PROXY_HOST:
strategies.append({"proxies": settings.PROXY, "timeout": timeout})
if get_runtime_setting('PROXY_HOST'):
strategies.append({"proxies": get_runtime_setting('PROXY'), "timeout": timeout})
strategies.append({"timeout": timeout})
for kwargs in strategies:
@@ -1124,8 +1124,8 @@ class SkillHelper(metaclass=WeakSingleton):
请求注册表 API兼容代理和直连场景
"""
strategies = []
if settings.PROXY_HOST:
strategies.append(({"proxies": settings.PROXY, "timeout": timeout}, url))
if get_runtime_setting('PROXY_HOST'):
strategies.append(({"proxies": get_runtime_setting('PROXY'), "timeout": timeout}, url))
strategies.append(({"timeout": timeout}, url))
for kwargs, target_url in strategies:
@@ -1152,17 +1152,17 @@ class SkillHelper(metaclass=WeakSingleton):
按代理优先级顺序请求 GitHub 资源兼容代理和直连场景
"""
strategies = []
headers = settings.REPO_GITHUB_HEADERS(repo=repo_name)
if not is_api and settings.GITHUB_PROXY:
proxy_url = f"{UrlUtils.standardize_base_url(settings.GITHUB_PROXY)}{url}"
headers = get_runtime_setting('REPO_GITHUB_HEADERS')(repo=repo_name)
if not is_api and get_runtime_setting('GITHUB_PROXY'):
proxy_url = f"{UrlUtils.standardize_base_url(get_runtime_setting('GITHUB_PROXY'))}{url}"
strategies.append((proxy_url, {"headers": headers, "timeout": timeout}))
if settings.PROXY_HOST:
if get_runtime_setting('PROXY_HOST'):
strategies.append(
(
url,
{
"headers": headers,
"proxies": settings.PROXY,
"proxies": get_runtime_setting('PROXY'),
"timeout": timeout,
},
)
+5 -6
View File
@@ -19,9 +19,8 @@ from app.agent.policy.sanitizer import (
)
from app.agent.tools.tags import ToolTag
from app.chain import ChainBase
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.application.messaging.agent import matches_channel_admin
from app.application.notification import get_notification_configs
from app.runtime.log import logger
@@ -301,7 +300,7 @@ class ToolExecutionTimeoutError(TimeoutError):
def _get_tool_timeout_seconds() -> Optional[float]:
"""读取工具执行超时时间,配置为 0 或负数时表示不限制。"""
try:
timeout = float(settings.LLM_TOOL_TIMEOUT or 0)
timeout = float(get_runtime_setting('LLM_TOOL_TIMEOUT') or 0)
except (TypeError, ValueError):
timeout = 0
return timeout if timeout > 0 else None
@@ -414,7 +413,7 @@ class MoviePilotTool(BaseTool, metaclass=ABCMeta):
# 发送工具执行过程消息(流式传输且非最后终结工具时)
if self._stream_handler and self._stream_handler.is_streaming and not self.return_direct:
if settings.AI_AGENT_VERBOSE:
if get_runtime_setting('AI_AGENT_VERBOSE'):
if self._stream_handler.is_auto_flushing:
# 渠道支持编辑:工具消息追加到 buffer,由定时刷新推送
if tool_message:
@@ -614,7 +613,7 @@ class MoviePilotTool(BaseTool, metaclass=ABCMeta):
:return: 普通用户允许读写的本地目录列表
"""
roots = [
settings.CONFIG_PATH / "agent",
get_runtime_setting('CONFIG_PATH') / "agent",
]
resolved_roots = []
for root in roots:
@@ -758,7 +757,7 @@ class MoviePilotTool(BaseTool, metaclass=ABCMeta):
"userid": None,
"username": message.username
or self._username
or settings.SUPERUSER,
or get_runtime_setting('SUPERUSER'),
"original_message_id": None,
"original_chat_id": None,
}
+2 -3
View File
@@ -9,11 +9,10 @@ from app.adapters.external.market import PluginHelper
from app.application.configuration import get_configured_system_config
from app.application.plugin.gateway import get_plugin_install_service
from app.application.plugin.runtime import get_plugin_manager
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
from app.schemas.plugin import PluginRuntimeStatus
from app.schemas.types import SystemConfigKey
settings = RuntimeSettingsCompat()
# 默认只向智能体返回一个可读预览,避免超大插件数据挤爆上下文窗口。
DEFAULT_PLUGIN_DATA_PREVIEW_CHARS = 12_000
@@ -395,7 +394,7 @@ async def uninstall_plugin_runtime(plugin_id: str) -> dict[str, Any]:
elif was_clone:
plugin_manager.delete_plugin_config(plugin_id)
plugin_manager.delete_plugin_data(plugin_id)
plugin_base_dir = settings.ROOT_PATH / "app" / "plugins" / plugin_id.lower()
plugin_base_dir = get_runtime_setting('ROOT_PATH') / "app" / "plugins" / plugin_id.lower()
try:
clone_files_removed = await run_agent_blocking(
"plugin",
+3 -5
View File
@@ -14,10 +14,8 @@ from pathlib import Path
from typing import Any, Optional
from app.agent.tools.impl._command_safety import validate_command_safety
from app.runtime.settings import RuntimeSettingsCompat
settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.runtime.settings import get_runtime_setting
if os.name == "posix":
import fcntl as _fcntl
@@ -151,10 +149,10 @@ class _TerminalSessionManager:
def _normalize_cwd(cwd: Optional[str]) -> str:
"""解析工作目录,未传入时默认使用 MoviePilot 项目根目录。"""
if not cwd:
return str(settings.ROOT_PATH)
return str(get_runtime_setting('ROOT_PATH'))
path = Path(cwd).expanduser()
if not path.is_absolute():
path = (settings.ROOT_PATH / path).resolve()
path = (get_runtime_setting('ROOT_PATH') / path).resolve()
else:
path = path.resolve()
if not path.exists():
+2 -3
View File
@@ -12,9 +12,8 @@ from app.agent.tools.tags import ToolTag
from app.chain.download import DownloadChain
from app.chain.media import MediaChain
from app.chain.search import SearchChain
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.domain.context import Context
from app.domain.metainfo import MetaInfo
from app.application.agentdata import get_agent_site_port
@@ -142,7 +141,7 @@ class AddDownloadTasksTool(MoviePilotTool):
@staticmethod
def _merge_labels_with_system_tag(labels: Optional[str]) -> Optional[str]:
"""合并用户标签与系统默认标签,确保任务可被系统管理"""
system_tag = (settings.TORRENT_TAG or "").strip()
system_tag = (get_runtime_setting('TORRENT_TAG') or "").strip()
user_labels = [item.strip() for item in (labels or "").split(",") if item.strip()]
if system_tag and system_tag not in user_labels:
+6 -7
View File
@@ -7,9 +7,8 @@ from pydantic import BaseModel, Field, model_validator
from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.application.agentdata import get_agent_chat_port
from app.application.agentdata import get_agent_task_port
from app.runtime.scheduling import TimerUtils
@@ -73,7 +72,7 @@ class CreateAgentTaskInput(BaseModel):
self.trigger_type, self.trigger = TimerUtils.normalize_schedule_trigger(
trigger_type=self.trigger_type,
trigger_value=self.trigger,
timezone_name=settings.TZ,
timezone_name=get_runtime_setting('TZ'),
require_future=True,
)
return self
@@ -105,14 +104,14 @@ class CreateAgentTaskTool(MoviePilotTool):
trigger_value = payload.trigger
if payload.trigger_type == "date" and payload.delay_minutes is not None:
timezone = pytz.timezone(settings.TZ)
timezone = pytz.timezone(get_runtime_setting('TZ'))
trigger_value = (
datetime.now(timezone) + timedelta(minutes=payload.delay_minutes)
).isoformat(timespec="seconds")
_, trigger_value = TimerUtils.normalize_schedule_trigger(
trigger_type=payload.trigger_type,
trigger_value=trigger_value,
timezone_name=settings.TZ,
timezone_name=get_runtime_setting('TZ'),
require_future=True,
)
chat = get_agent_chat_port().get(
@@ -136,7 +135,7 @@ class CreateAgentTaskTool(MoviePilotTool):
return get_agent_task_port().to_dict(
task,
next_run_at=next_run_at,
timezone=settings.TZ,
timezone=get_runtime_setting('TZ'),
)
async def run(
@@ -149,7 +148,7 @@ class CreateAgentTaskTool(MoviePilotTool):
**kwargs: object,
) -> str:
"""创建 Agent 自主定时任务。"""
if not settings.AI_AGENT_ENABLE:
if not get_runtime_setting('AI_AGENT_ENABLE'):
return "AI Agent 未启用,无法创建自主定时任务"
payload = CreateAgentTaskInput(
name=name,
+2 -4
View File
@@ -5,10 +5,8 @@ from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.runtime.settings import RuntimeSettingsCompat
settings = RuntimeSettingsCompat()
from app.application.agentdata import get_agent_task_port
from app.runtime.settings import get_runtime_setting
class QueryAgentTasksInput(BaseModel):
@@ -63,7 +61,7 @@ class QueryAgentTasksTool(MoviePilotTool):
data = oper.to_dict(
task,
next_run_at=get_agent_task_next_run(task.id),
timezone=settings.TZ,
timezone=get_runtime_setting('TZ'),
)
if task_id:
data["recent_runs"] = [
+2 -3
View File
@@ -9,9 +9,8 @@ from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.media import MediaChain
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.domain.context import Context
from app.domain.meta.metamusic import MetaMusic
from app.domain.metainfo import MetaInfo
@@ -104,7 +103,7 @@ class RecognizeMediaTool(MoviePilotTool):
}, ensure_ascii=False)
is_audio_path = bool(
path and Path(path).suffix.lower() in settings.RMT_AUDIOEXT
path and Path(path).suffix.lower() in get_runtime_setting('RMT_AUDIOEXT')
)
recognize_music = media_type_enum == MediaType.MUSIC or (
media_type_enum is None and is_audio_path
+2 -3
View File
@@ -10,9 +10,8 @@ from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.media import MediaChain
from app.chain.scraping import ScrapingChain
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.schemas.workflow import FileItem
from app.schemas.types import (
@@ -184,7 +183,7 @@ class ScrapeMetadataTool(MoviePilotTool):
scraping_chain = ScrapingChain()
is_audio_file = (
fileitem.type == "file"
and Path(path).suffix.lower() in settings.RMT_AUDIOEXT
and Path(path).suffix.lower() in get_runtime_setting('RMT_AUDIOEXT')
)
scrape_music = media_type_enum == MediaType.MUSIC or (
media_type_enum is None and is_audio_file
+2 -4
View File
@@ -9,10 +9,8 @@ from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.runtime.settings import RuntimeSettingsCompat
settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.runtime.settings import get_runtime_setting
# 搜索超时时间(秒)
SEARCH_TIMEOUT = 20
@@ -412,7 +410,7 @@ class SearchWebTool(MoviePilotTool):
"""在线程中执行同步搜索"""
results = []
ddgs_kwargs = {"timeout": SEARCH_TIMEOUT}
proxy_url = self._get_proxy_url(settings.PROXY)
proxy_url = self._get_proxy_url(get_runtime_setting('PROXY'))
if proxy_url:
ddgs_kwargs["proxy"] = proxy_url
+2 -3
View File
@@ -6,9 +6,8 @@ from pydantic import BaseModel, Field
from app.agent.llm.capability import AgentCapabilityManager
from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.schemas.message import Message
from app.schemas.message import MessageType
@@ -96,7 +95,7 @@ class SendVoiceMessageTool(MoviePilotTool):
voice_path=voice_path,
voice_caption=(
message
if voice_path and settings.AUDIO_OUTPUT_INCLUDE_TEXT
if voice_path and get_runtime_setting('AUDIO_OUTPUT_INCLUDE_TEXT')
else None
),
save_history=False,
+4 -6
View File
@@ -7,11 +7,9 @@ from pydantic import BaseModel, Field, model_validator
from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.runtime.settings import RuntimeSettingsCompat
settings = RuntimeSettingsCompat()
from app.application.agentdata import get_agent_task_port
from app.runtime.scheduling import TimerUtils
from app.runtime.settings import get_runtime_setting
class UpdateAgentTaskInput(BaseModel):
@@ -115,7 +113,7 @@ class UpdateAgentTaskTool(MoviePilotTool):
trigger_type = payload.trigger_type or task.trigger_type
trigger_value = payload.trigger
if trigger_type == "date" and payload.delay_minutes is not None:
timezone = pytz.timezone(settings.TZ)
timezone = pytz.timezone(get_runtime_setting('TZ'))
trigger_value = (
datetime.now(timezone) + timedelta(minutes=payload.delay_minutes)
).isoformat(timespec="seconds")
@@ -134,7 +132,7 @@ class UpdateAgentTaskTool(MoviePilotTool):
normalized_type, normalized_trigger = TimerUtils.normalize_schedule_trigger(
trigger_type=trigger_type,
trigger_value=trigger_value,
timezone_name=settings.TZ,
timezone_name=get_runtime_setting('TZ'),
require_future=bool(
trigger_type == "date"
and (
@@ -181,7 +179,7 @@ class UpdateAgentTaskTool(MoviePilotTool):
return oper.to_dict(
updated_task,
next_run_at=next_run_at,
timezone=settings.TZ,
timezone=get_runtime_setting('TZ'),
)
async def run(
-3
View File
@@ -396,9 +396,6 @@ def configure_runtime_settings(service: RuntimeSettingsService) -> None:
"""由组合根登记管理 API 使用的部署设置服务。"""
global _runtime_settings_service
_runtime_settings_service = service
from app.runtime.settings import configure_runtime_settings_compat
configure_runtime_settings_compat(service)
def get_runtime_settings() -> RuntimeSettingsService:
+6 -5
View File
@@ -6,19 +6,20 @@ from urllib.parse import urlencode
from app.adapters.network.http import AsyncRequestUtils, RequestUtils
from app.runtime.log import logger
from app.runtime.settings import RuntimeSettingsCompat
settings = RuntimeSettingsCompat()
from app.runtime.settings import get_runtime_setting
BackgroundSubmitter = Callable[..., object]
def build_message_ingress_url(source: str | None) -> str:
"""按当前运行配置构造安全编码的本地消息入口 URL。"""
query = {"token": settings.API_TOKEN}
query = {"token": get_runtime_setting('API_TOKEN')}
if source:
query["source"] = source
return f"http://127.0.0.1:{settings.PORT}/api/v1/message?{urlencode(query)}"
return (
f"http://127.0.0.1:{get_runtime_setting('PORT')}/api/v1/message?"
f"{urlencode(query)}"
)
def forward_message_to_host(
+51 -49
View File
@@ -16,21 +16,20 @@ from urllib.request import Request, urlopen
import click
import psutil
from app.runtime.config import Settings
from app.runtime.settings import RuntimeSettingsCompat
settings = RuntimeSettingsCompat()
from app.runtime.state import SystemHelper
from app.application.backup import BackupArtifact
from app.startup.composition.database import build_database_governance
from app.application.configuration import get_runtime_settings
from app.runtime.config import Settings
from app.runtime.settings import get_runtime_setting
from app.runtime.state import SystemHelper
from app.runtime.version import get_app_version, get_frontend_version
from app.startup.composition.database import build_database_governance
BACKEND_RUNTIME_FILE = settings.TEMP_PATH / "moviepilot.runtime.json"
BACKEND_STDIO_LOG_FILE = settings.LOG_PATH / "moviepilot.stdout.log"
BACKEND_APP_LOG_FILE = settings.LOG_PATH / "moviepilot.log"
FRONTEND_RUNTIME_FILE = settings.TEMP_PATH / "moviepilot.frontend.runtime.json"
FRONTEND_STDIO_LOG_FILE = settings.LOG_PATH / "moviepilot.frontend.stdout.log"
FRONTEND_DIR = settings.ROOT_PATH / "public"
BACKEND_RUNTIME_FILE = get_runtime_setting('TEMP_PATH') / "moviepilot.runtime.json"
BACKEND_STDIO_LOG_FILE = get_runtime_setting('LOG_PATH') / "moviepilot.stdout.log"
BACKEND_APP_LOG_FILE = get_runtime_setting('LOG_PATH') / "moviepilot.log"
FRONTEND_RUNTIME_FILE = get_runtime_setting('TEMP_PATH') / "moviepilot.frontend.runtime.json"
FRONTEND_STDIO_LOG_FILE = get_runtime_setting('LOG_PATH') / "moviepilot.frontend.stdout.log"
FRONTEND_DIR = get_runtime_setting('ROOT_PATH') / "public"
FRONTEND_SERVICE_FILE = FRONTEND_DIR / "service.js"
FRONTEND_VERSION_FILE = FRONTEND_DIR / "version.txt"
HEALTH_PATH = "/api/v1/system/global"
@@ -47,13 +46,13 @@ MASKED_FIELDS = {
}
MASKED_SUFFIXES = ("_TOKEN", "_PASSWORD", "_SECRET", "_API_KEY")
CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
PREPARED_UPDATE_ROOT = settings.TEMP_PATH / "moviepilot-update"
PREPARED_UPDATE_ROOT = get_runtime_setting('TEMP_PATH') / "moviepilot-update"
PREPARED_UPDATE_MANIFEST = PREPARED_UPDATE_ROOT / "install.json"
PREPARED_UPDATE_STATE = PREPARED_UPDATE_ROOT / "state.json"
def _repo_root() -> Path:
return settings.ROOT_PATH
return get_runtime_setting('ROOT_PATH')
def _read_json_file(path: Path) -> Optional[Dict[str, Any]]:
@@ -115,21 +114,21 @@ def _frontend_runtime() -> Optional[Dict[str, Any]]:
def _backend_base_url(runtime: Optional[Dict[str, Any]] = None) -> str:
runtime = runtime or _backend_runtime() or {}
host = runtime.get("host") or settings.HOST
port = runtime.get("port") or settings.PORT
host = runtime.get("host") or get_runtime_setting('HOST')
port = runtime.get("port") or get_runtime_setting('PORT')
return f"http://{_client_host(host)}:{port}"
def _frontend_base_url(runtime: Optional[Dict[str, Any]] = None) -> str:
runtime = runtime or _frontend_runtime() or {}
host = runtime.get("host") or settings.HOST
port = runtime.get("port") or settings.NGINX_PORT
host = runtime.get("host") or get_runtime_setting('HOST')
port = runtime.get("port") or get_runtime_setting('NGINX_PORT')
return f"http://{_client_host(host)}:{port}"
def _runtime_api_token(runtime: Optional[Dict[str, Any]] = None) -> str:
runtime = runtime or _backend_runtime() or {}
return runtime.get("api_token") or settings.API_TOKEN
return runtime.get("api_token") or get_runtime_setting('API_TOKEN')
def _http_request(
@@ -238,7 +237,7 @@ def _git_current_branch() -> Optional[str]:
def _auto_update_mode() -> str:
if SystemHelper.consume_one_shot_dev_update():
return "dev"
return str(settings.MOVIEPILOT_AUTO_UPDATE or "").strip().lower()
return str(get_runtime_setting('MOVIEPILOT_AUTO_UPDATE') or "").strip().lower()
def _file_sha256(path: Path) -> str:
@@ -267,18 +266,18 @@ def _local_update_env() -> dict[str, str]:
"""构造本地更新子进程使用的包缓存、代理和认证环境。"""
update_env = os.environ.copy()
package_cache_root = Path(
update_env.get("PACKAGE_CACHE_ROOT", "").strip() or settings.PACKAGE_CACHE_PATH
update_env.get("PACKAGE_CACHE_ROOT", "").strip() or get_runtime_setting('PACKAGE_CACHE_PATH')
)
update_env.setdefault("PACKAGE_CACHE_ROOT", str(package_cache_root))
update_env.setdefault("UV_CACHE_DIR", str(package_cache_root / "uv"))
if settings.PIP_PROXY:
update_env["PIP_PROXY"] = settings.PIP_PROXY
if settings.PROXY_HOST:
update_env["PROXY_HOST"] = settings.PROXY_HOST
if get_runtime_setting('PIP_PROXY'):
update_env["PIP_PROXY"] = get_runtime_setting('PIP_PROXY')
if get_runtime_setting('PROXY_HOST'):
update_env["PROXY_HOST"] = get_runtime_setting('PROXY_HOST')
for key in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"):
update_env[key] = settings.PROXY_HOST
if settings.GITHUB_TOKEN:
update_env.setdefault("GITHUB_TOKEN", settings.GITHUB_TOKEN)
update_env[key] = get_runtime_setting('PROXY_HOST')
if get_runtime_setting('GITHUB_TOKEN'):
update_env.setdefault("GITHUB_TOKEN", get_runtime_setting('GITHUB_TOKEN'))
return update_env
@@ -322,7 +321,7 @@ def _apply_prepared_release_update() -> bool:
"--venv",
str(_repo_root() / "venv"),
"--config-dir",
str(settings.CONFIG_PATH),
str(get_runtime_setting('CONFIG_PATH')),
]
click.echo(f"安装已下载并校验的 MoviePilot {version} 更新包")
result = subprocess.run(
@@ -391,7 +390,7 @@ def _best_effort_auto_update() -> None:
"--venv",
str(_repo_root() / "venv"),
"--config-dir",
str(settings.CONFIG_PATH),
str(get_runtime_setting('CONFIG_PATH')),
]
click.echo(f"检测到 MOVIEPILOT_AUTO_UPDATE={mode},启动前执行本地自动更新")
@@ -647,10 +646,12 @@ def _parse_key_value_pairs(items: Iterable[str]) -> Dict[str, str]:
def _ensure_local_api_token() -> bool:
if settings.API_TOKEN and len(str(settings.API_TOKEN).strip()) >= 16:
if get_runtime_setting('API_TOKEN') and len(str(get_runtime_setting('API_TOKEN')).strip()) >= 16:
return False
result, message = settings.update_setting("API_TOKEN", settings.API_TOKEN or "")
result, message = get_runtime_settings().update(
"API_TOKEN", get_runtime_setting('API_TOKEN') or ""
)
if result is False:
raise click.ClickException(message or "初始化 API_TOKEN 失败")
return result is True
@@ -691,10 +692,10 @@ def _spawn_backend_process(*, safe: bool = False) -> subprocess.Popen:
"MOVIEPILOT_DISABLE_CONSOLE_LOG": "1",
"MOVIEPILOT_STDIO_LOG_FILE": str(BACKEND_STDIO_LOG_FILE),
"MOVIEPILOT_STDIO_LOG_MAX_BYTES": str(
max(int(settings.LOG_MAX_FILE_SIZE or 0), 1) * 1024 * 1024
max(int(get_runtime_setting('LOG_MAX_FILE_SIZE') or 0), 1) * 1024 * 1024
),
"MOVIEPILOT_STDIO_LOG_BACKUP_COUNT": str(
max(int(settings.LOG_BACKUP_COUNT or 0), 0)
max(int(get_runtime_setting('LOG_BACKUP_COUNT') or 0), 0)
),
}
if safe:
@@ -741,7 +742,7 @@ def _spawn_frontend_process(backend_port: int) -> subprocess.Popen:
env={
**os.environ,
"PORT": str(backend_port),
"NGINX_PORT": str(settings.NGINX_PORT),
"NGINX_PORT": str(get_runtime_setting('NGINX_PORT')),
},
)
@@ -796,9 +797,9 @@ def _start_backend_service(timeout: int, safe: bool = False) -> Dict[str, Any]:
runtime = {
"pid": process.pid,
"create_time": ps_process.create_time(),
"host": settings.HOST,
"port": settings.PORT,
"api_token": settings.API_TOKEN,
"host": get_runtime_setting('HOST'),
"port": get_runtime_setting('PORT'),
"api_token": get_runtime_setting('API_TOKEN'),
"started_at": int(time.time()),
"python": sys.executable,
"stdio_log": str(BACKEND_STDIO_LOG_FILE),
@@ -822,8 +823,8 @@ def _start_frontend_service(timeout: int, backend_port: int) -> Dict[str, Any]:
runtime = {
"pid": process.pid,
"create_time": ps_process.create_time(),
"host": settings.HOST,
"port": settings.NGINX_PORT,
"host": get_runtime_setting('HOST'),
"port": get_runtime_setting('NGINX_PORT'),
"backend_port": backend_port,
"started_at": int(time.time()),
"node": str(_frontend_node_binary()),
@@ -1139,8 +1140,9 @@ def config() -> None:
@config.command("path", context_settings=CONTEXT_SETTINGS)
def config_path() -> None:
"""显示配置路径"""
click.echo(f"Config Dir: {settings.CONFIG_PATH}")
click.echo(f"Env File: {settings.CONFIG_PATH / 'app.env'}")
config_path = get_runtime_setting('CONFIG_PATH')
click.echo(f"Config Dir: {config_path}")
click.echo(f"Env File: {config_path / 'app.env'}")
click.echo(f"Frontend Dir: {FRONTEND_DIR}")
@@ -1148,7 +1150,7 @@ def config_path() -> None:
@click.option("--show-secrets", is_flag=True, help="显示敏感配置原文")
def config_list(show_secrets: bool) -> None:
"""列出当前配置"""
values = settings.model_dump()
values = get_runtime_settings().snapshot()
for key in sorted(values):
click.echo(f"{key}={_format_value(_mask_value(key, values[key], show_secrets))}")
@@ -1158,9 +1160,9 @@ def config_list(show_secrets: bool) -> None:
def config_get(key: str) -> None:
"""读取单个配置项"""
setting_fields = Settings.model_fields.keys()
if key not in setting_fields and not hasattr(settings, key):
if key not in setting_fields and not get_runtime_settings().contains(key):
raise click.ClickException(f"配置项不存在:{key}")
click.echo(_format_value(getattr(settings, key)))
click.echo(_format_value(get_runtime_settings().get(key)))
@config.command("set", context_settings=CONTEXT_SETTINGS)
@@ -1168,7 +1170,7 @@ def config_get(key: str) -> None:
@click.argument("value")
def config_set(key: str, value: str) -> None:
"""写入单个配置项"""
result, message = settings.update_setting(key, value)
result, message = get_runtime_settings().update(key, value)
if result is False:
raise click.ClickException(message or f"配置项更新失败:{key}")
if result is None:
@@ -1196,7 +1198,7 @@ def config_keys(pattern: Optional[str], show_current: bool, show_secrets: bool)
if pattern and pattern.lower() not in key.lower():
continue
default_value = _field_default(field)
current_value = getattr(settings, key, default_value)
current_value = get_runtime_settings().get(key, default_value)
rows.append(
(
key,
@@ -1228,12 +1230,12 @@ def config_describe(key: str, show_secrets: bool) -> None:
raise click.ClickException(f"配置项不存在:{key}")
default_value = _field_default(field)
current_value = getattr(settings, key, default_value)
current_value = get_runtime_settings().get(key, default_value)
click.echo(f"Key: {key}")
click.echo(f"Type: {_annotation_name(field.annotation)}")
click.echo(f"Default: {_format_value(_mask_value(key, default_value, show_secrets))}")
click.echo(f"Current: {_format_value(_mask_value(key, current_value, show_secrets))}")
click.echo(f"Env File: {settings.CONFIG_PATH / 'app.env'}")
click.echo(f"Env File: {get_runtime_setting('CONFIG_PATH') / 'app.env'}")
@cli.group(context_settings=CONTEXT_SETTINGS)
+10 -11
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Awaitable, Callable
from contextlib import AbstractAsyncContextManager
from typing import Any, TypeVar
from typing import Any, List, TypeVar
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
@@ -12,7 +12,6 @@ from sqlalchemy.orm import Session
from app.db.oper.site import SiteOper
from app.db.uow import SqlAlchemyAsyncUnitOfWork, SqlAlchemyUnitOfWork
T = TypeVar("T")
@@ -80,19 +79,19 @@ class TransactionalSiteRepository:
"""按域名查询站点。"""
return self._read(lambda repository: repository.get_by_domain(domain))
def get_domains_by_ids(self, ids: list[int]) -> list[str | None]:
def get_domains_by_ids(self, ids: List[int]) -> List[str | None]:
"""查询一组站点 ID 对应的域名。"""
return self._read(lambda repository: repository.get_domains_by_ids(ids))
def list(self) -> list[Any]:
def list(self) -> List[Any]:
"""查询全部站点。"""
return self._read(lambda repository: repository.list())
def list_order_by_pri(self) -> list[Any]:
def list_order_by_pri(self) -> List[Any]:
"""同步按优先级查询站点。"""
return self._read(lambda repository: repository.list_order_by_pri())
def get_userdata_latest(self) -> list[Any]:
def get_userdata_latest(self) -> List[Any]:
"""同步查询各站点最新用户数据。"""
return self._read(lambda repository: repository.get_userdata_latest())
@@ -112,11 +111,11 @@ class TransactionalSiteRepository:
lambda repository: repository.async_get_by_name(name)
)
async def async_list(self) -> list[Any]:
async def async_list(self) -> List[Any]:
"""异步查询全部站点。"""
return await self._async_read(lambda repository: repository.async_list())
async def async_list_order_by_pri(self) -> list[Any]:
async def async_list_order_by_pri(self) -> List[Any]:
"""异步按优先级查询站点。"""
return await self._async_read(
lambda repository: repository.async_list_order_by_pri()
@@ -132,13 +131,13 @@ class TransactionalSiteRepository:
self,
domain: str,
workdate: str | None = None,
) -> list[Any]:
) -> List[Any]:
"""异步查询站点用户数据。"""
return await self._async_read(
lambda repository: repository.async_get_userdata_by_domain(domain, workdate)
)
async def async_get_userdata_latest(self) -> list[Any]:
async def async_get_userdata_latest(self) -> List[Any]:
"""异步查询各站点最新用户数据。"""
return await self._async_read(
lambda repository: repository.async_get_userdata_latest()
@@ -156,7 +155,7 @@ class TransactionalSiteRepository:
lambda repository: repository.async_get_statistic_by_domain(domain)
)
async def async_list_statistics(self) -> list[Any]:
async def async_list_statistics(self) -> List[Any]:
"""异步查询全部站点统计。"""
return await self._async_read(
lambda repository: repository.async_list_statistics()
+5 -4
View File
@@ -3,6 +3,7 @@
from collections.abc import Callable
from contextlib import AbstractAsyncContextManager
from datetime import datetime, timezone
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
@@ -41,8 +42,8 @@ class TransactionalSubscribeWriter:
def add(
self,
identity: dict,
payload: dict,
identity: dict[str, Any],
payload: dict[str, Any],
username: str | None = None,
after_commit: AfterCommitEffect | None = None,
notification: dict[str, object] | None = None,
@@ -88,8 +89,8 @@ class TransactionalSubscribeWriter:
async def async_add(
self,
identity: dict,
payload: dict,
identity: dict[str, Any],
payload: dict[str, Any],
username: str | None = None,
after_commit: AsyncAfterCommitEffect | None = None,
notification: dict[str, object] | None = None,
+2 -2
View File
@@ -13,7 +13,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, declared_attr, mapped_column
from app.db.uow import run_async_transaction, run_sync_transaction
from app.runtime.config import settings
from app.runtime.settings import get_runtime_setting
T = TypeVar("T")
@@ -43,7 +43,7 @@ def get_id_column() -> Mapped[int]:
"""
根据数据库类型返回合适的ID列定义
"""
if settings.DB_TYPE.lower() == "postgresql":
if get_runtime_setting('DB_TYPE').lower() == "postgresql":
# PostgreSQL使用SERIAL类型,让数据库自动处理序列
return mapped_column(Integer, Identity(start=1, cycle=True), primary_key=True)
else:
+47 -47
View File
@@ -13,7 +13,7 @@ from sqlalchemy.ext.asyncio import AsyncEngine as SaAsyncEngine, create_async_en
from sqlalchemy.pool import Pool
from app.foundation.environment import is_free_threaded_runtime
from app.runtime.config import settings
from app.runtime.settings import get_runtime_setting
from app.db.diagnostics import _register_database_error_logging
from app.db.worker import DATABASE_WORKER_MAX_WORKERS
from app.runtime.log import logger
@@ -22,7 +22,7 @@ from app.runtime.observability import record_metric
def _database_backend_label() -> str:
"""把数据库类型收敛为有限的观测标签。"""
return "postgresql" if settings.DB_TYPE.lower() == "postgresql" else "sqlite"
return "postgresql" if get_runtime_setting('DB_TYPE').lower() == "postgresql" else "sqlite"
def _sync_postgresql_driver() -> Optional[str]:
@@ -61,9 +61,9 @@ def _async_pool_kwargs(pooled: bool) -> dict:
if not pooled:
return {"poolclass": NullPool}
return {
"pool_size": settings.DB_ASYNC_POOL_SIZE,
"max_overflow": settings.DB_ASYNC_MAX_OVERFLOW,
"pool_timeout": settings.DB_POOL_TIMEOUT,
"pool_size": get_runtime_setting('DB_ASYNC_POOL_SIZE'),
"max_overflow": get_runtime_setting('DB_ASYNC_MAX_OVERFLOW'),
"pool_timeout": get_runtime_setting('DB_POOL_TIMEOUT'),
}
@@ -75,7 +75,7 @@ def _get_database_engine(is_async: bool = False, pooled: bool = False):
:return: 返回对应的数据库引擎
"""
# 根据数据库类型选择连接方式
if settings.DB_TYPE.lower() == "postgresql":
if get_runtime_setting('DB_TYPE').lower() == "postgresql":
return _get_postgresql_engine(is_async, pooled=pooled)
else:
return _get_sqlite_engine(is_async, pooled=pooled)
@@ -87,35 +87,35 @@ def _get_sqlite_engine(is_async: bool = False, pooled: bool = False):
"""
# 连接参数
_connect_args = {
"timeout": settings.DB_TIMEOUT,
"timeout": get_runtime_setting('DB_TIMEOUT'),
}
# 允许部署侧注入驱动级参数(如 PgBouncer 事务模式下的 statement_cache_size
_connect_args.update(settings.DB_CONNECT_ARGS or {})
_connect_args.update(get_runtime_setting('DB_CONNECT_ARGS') or {})
# 启用 WAL 模式时的额外配置
if settings.DB_WAL_ENABLE:
if get_runtime_setting('DB_WAL_ENABLE'):
_connect_args["check_same_thread"] = False
# 创建同步引擎
if not is_async:
# 根据池类型设置 poolclass 和相关参数
_pool_class = NullPool if settings.DB_POOL_TYPE == "NullPool" else QueuePool
_pool_class = NullPool if get_runtime_setting('DB_POOL_TYPE') == "NullPool" else QueuePool
# 数据库参数
_db_kwargs = {
"url": settings.DB_SQLITE_URL(),
"pool_pre_ping": settings.DB_POOL_PRE_PING,
"echo": settings.DB_ECHO,
"url": get_runtime_setting('DB_SQLITE_URL')(),
"pool_pre_ping": get_runtime_setting('DB_POOL_PRE_PING'),
"echo": get_runtime_setting('DB_ECHO'),
"poolclass": _pool_class,
"pool_recycle": settings.DB_POOL_RECYCLE,
"pool_recycle": get_runtime_setting('DB_POOL_RECYCLE'),
"connect_args": _connect_args
}
# 当使用 QueuePool 时,添加 QueuePool 特有的参数
if _pool_class == QueuePool:
_db_kwargs.update({
"pool_size": settings.DB_SQLITE_POOL_SIZE,
"pool_timeout": settings.DB_POOL_TIMEOUT,
"max_overflow": settings.DB_SQLITE_MAX_OVERFLOW
"pool_size": get_runtime_setting('DB_SQLITE_POOL_SIZE'),
"pool_timeout": get_runtime_setting('DB_POOL_TIMEOUT'),
"max_overflow": get_runtime_setting('DB_SQLITE_MAX_OVERFLOW')
})
# 创建数据库引擎
@@ -129,7 +129,7 @@ def _get_sqlite_engine(is_async: bool = False, pooled: bool = False):
# 设置一次,而同步引擎的首次创建由 lifespan 数据库准备组件中的 init_db() 完成,
# 不存在一群线程
# 等在锁上的场面;即便退化到运行期首次访问,阻塞的也只是本地 SQLite 的一次 PRAGMA。
_journal_mode = "WAL" if settings.DB_WAL_ENABLE else "DELETE"
_journal_mode = "WAL" if get_runtime_setting('DB_WAL_ENABLE') else "DELETE"
with engine.connect() as connection:
current_mode = connection.execute(text(f"PRAGMA journal_mode={_journal_mode};")).scalar()
print(f"SQLite database journal mode set to: {current_mode}")
@@ -138,10 +138,10 @@ def _get_sqlite_engine(is_async: bool = False, pooled: bool = False):
else:
# 数据库参数,只能使用 NullPool
_db_kwargs = {
"url": settings.DB_SQLITE_URL("aiosqlite"),
"pool_pre_ping": settings.DB_POOL_PRE_PING,
"echo": settings.DB_ECHO,
"pool_recycle": settings.DB_POOL_RECYCLE,
"url": get_runtime_setting('DB_SQLITE_URL')("aiosqlite"),
"pool_pre_ping": get_runtime_setting('DB_POOL_PRE_PING'),
"echo": get_runtime_setting('DB_ECHO'),
"pool_recycle": get_runtime_setting('DB_POOL_RECYCLE'),
"connect_args": _connect_args,
**_async_pool_kwargs(pooled),
}
@@ -162,51 +162,51 @@ def _get_postgresql_engine(is_async: bool = False, pooled: bool = False):
"""
获取PostgreSQL数据库引擎
"""
db_url = settings.DB_POSTGRESQL_URL(_sync_postgresql_driver())
db_url = get_runtime_setting('DB_POSTGRESQL_URL')(_sync_postgresql_driver())
# PostgreSQL连接参数。允许部署侧注入驱动级参数,
# 例如经 PgBouncer 事务模式接入时 asyncpg 需要 statement_cache_size=0
_connect_args = dict(settings.DB_CONNECT_ARGS or {})
_connect_args = dict(get_runtime_setting('DB_CONNECT_ARGS') or {})
# 创建同步引擎
if not is_async:
# 根据池类型设置 poolclass 和相关参数
_pool_class = NullPool if settings.DB_POOL_TYPE == "NullPool" else QueuePool
_pool_class = NullPool if get_runtime_setting('DB_POOL_TYPE') == "NullPool" else QueuePool
# 数据库参数
_db_kwargs = {
"url": db_url,
"pool_pre_ping": settings.DB_POOL_PRE_PING,
"echo": settings.DB_ECHO,
"pool_pre_ping": get_runtime_setting('DB_POOL_PRE_PING'),
"echo": get_runtime_setting('DB_ECHO'),
"poolclass": _pool_class,
"pool_recycle": settings.DB_POOL_RECYCLE,
"pool_recycle": get_runtime_setting('DB_POOL_RECYCLE'),
"connect_args": _connect_args
}
# 当使用 QueuePool 时,添加 QueuePool 特有的参数
if _pool_class == QueuePool:
_db_kwargs.update({
"pool_size": settings.DB_POSTGRESQL_POOL_SIZE,
"pool_timeout": settings.DB_POOL_TIMEOUT,
"max_overflow": settings.DB_POSTGRESQL_MAX_OVERFLOW
"pool_size": get_runtime_setting('DB_POSTGRESQL_POOL_SIZE'),
"pool_timeout": get_runtime_setting('DB_POOL_TIMEOUT'),
"max_overflow": get_runtime_setting('DB_POSTGRESQL_MAX_OVERFLOW')
})
# 创建数据库引擎
engine = create_engine(**_db_kwargs)
_register_database_error_logging(engine)
_register_database_pool_metrics(engine)
print(f"PostgreSQL database connected to {settings.DB_POSTGRESQL_TARGET}/{settings.DB_POSTGRESQL_DATABASE}")
print(f"PostgreSQL database connected to {get_runtime_setting('DB_POSTGRESQL_TARGET')}/{get_runtime_setting('DB_POSTGRESQL_DATABASE')}")
return engine
else:
async_db_url = settings.DB_POSTGRESQL_URL("asyncpg")
async_db_url = get_runtime_setting('DB_POSTGRESQL_URL')("asyncpg")
# 数据库参数,只能使用 NullPool
_db_kwargs = {
"url": async_db_url,
"pool_pre_ping": settings.DB_POOL_PRE_PING,
"echo": settings.DB_ECHO,
"pool_recycle": settings.DB_POOL_RECYCLE,
"pool_pre_ping": get_runtime_setting('DB_POOL_PRE_PING'),
"echo": get_runtime_setting('DB_ECHO'),
"pool_recycle": get_runtime_setting('DB_POOL_RECYCLE'),
"connect_args": _connect_args,
**_async_pool_kwargs(pooled),
}
@@ -214,7 +214,7 @@ def _get_postgresql_engine(is_async: bool = False, pooled: bool = False):
async_engine = create_async_engine(**_db_kwargs)
_register_database_error_logging(async_engine.sync_engine)
_register_database_pool_metrics(async_engine.sync_engine)
print(f"Async PostgreSQL database connected to {settings.DB_POSTGRESQL_TARGET}/{settings.DB_POSTGRESQL_DATABASE}")
print(f"Async PostgreSQL database connected to {get_runtime_setting('DB_POSTGRESQL_TARGET')}/{get_runtime_setting('DB_POSTGRESQL_DATABASE')}")
return async_engine
@@ -291,7 +291,7 @@ def _async_pool_enabled() -> bool:
"""
是否启用异步连接池设为 NullPool 可回退到池化前的行为
"""
return str(settings.DB_ASYNC_POOL_TYPE or "").strip().lower() != "nullpool"
return str(get_runtime_setting('DB_ASYNC_POOL_TYPE') or "").strip().lower() != "nullpool"
def connection_budget() -> Dict[str, int]:
@@ -306,19 +306,19 @@ def connection_budget() -> Dict[str, int]:
就顶穿了 max_connections
:return: 单进程各项上限worker 数与合计
"""
if settings.DB_TYPE.lower() == "postgresql":
sync_max = settings.DB_POSTGRESQL_POOL_SIZE + settings.DB_POSTGRESQL_MAX_OVERFLOW
if get_runtime_setting('DB_TYPE').lower() == "postgresql":
sync_max = get_runtime_setting('DB_POSTGRESQL_POOL_SIZE') + get_runtime_setting('DB_POSTGRESQL_MAX_OVERFLOW')
else:
sync_max = settings.DB_SQLITE_POOL_SIZE + settings.DB_SQLITE_MAX_OVERFLOW
if settings.DB_POOL_TYPE == "NullPool":
sync_max = get_runtime_setting('DB_SQLITE_POOL_SIZE') + get_runtime_setting('DB_SQLITE_MAX_OVERFLOW')
if get_runtime_setting('DB_POOL_TYPE') == "NullPool":
# 未池化连接由通用线程池和专属数据库 worker 共同创建,二者都要计入上限估计。
sync_max = settings.CONF.threadpool + DATABASE_WORKER_MAX_WORKERS
async_max = (settings.DB_ASYNC_POOL_SIZE + settings.DB_ASYNC_MAX_OVERFLOW
sync_max = get_runtime_setting('CONF').threadpool + DATABASE_WORKER_MAX_WORKERS
async_max = (get_runtime_setting('DB_ASYNC_POOL_SIZE') + get_runtime_setting('DB_ASYNC_MAX_OVERFLOW')
if _async_pool_enabled() else 0)
fallback = settings.DB_ASYNC_FALLBACK_LIMIT if _async_pool_enabled() else settings.CONF.scheduler
fallback = get_runtime_setting('DB_ASYNC_FALLBACK_LIMIT') if _async_pool_enabled() else get_runtime_setting('CONF').scheduler
per_worker = sync_max + async_max + fallback
# worker 数非法时按 1 计:退化成 0 会让合计归零、反而误判「额度充足」
workers = getattr(settings, "API_WORKERS", 1) or 1
workers = get_runtime_setting("API_WORKERS", 1) or 1
workers = workers if isinstance(workers, int) and workers > 0 else 1
return {
"sync": sync_max,
@@ -339,7 +339,7 @@ def check_connection_budget() -> bool:
:return: 是否在额度之内
"""
budget = connection_budget()
if settings.DB_TYPE.lower() != "postgresql":
if get_runtime_setting('DB_TYPE').lower() != "postgresql":
logger.info(f"数据库连接理论峰值: {budget['total']} "
f"(单进程 {budget['per_worker']} = 同步 {budget['sync']} + 异步池 "
f"{budget['async_pooled']} + 回退 {budget['async_fallback']}"
+18 -15
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from typing import Optional
from typing import Any, List, Optional, cast
from uuid import uuid4
from sqlalchemy import select
@@ -65,13 +65,16 @@ class AgentTaskOper(DbOper):
"""
def query(session: Session) -> Optional[AgentTask]:
"""在调用方会话中读取单个任务。"""
return session.execute(
_get_for_user_statement(
AgentTask,
task_id=task_id,
user_id=user_id,
)
).scalars().first()
return cast(
Optional[AgentTask],
session.execute(
_get_for_user_statement(
AgentTask,
task_id=task_id,
user_id=user_id,
)
).scalars().first(),
)
return self._execute_sync_query(query)
@@ -90,7 +93,7 @@ class AgentTaskOper(DbOper):
user_id=user_id,
)
)
return result.scalars().first()
return cast(Optional[AgentTask], result.scalars().first())
return await self._execute_async_query(query)
@@ -98,11 +101,11 @@ class AgentTaskOper(DbOper):
self,
user_id: Optional[str] = None,
enabled: Optional[bool] = None,
) -> list[AgentTask]:
) -> List[AgentTask]:
"""
查询 Agent 定时任务列表
"""
def query(session: Session) -> list[AgentTask]:
def query(session: Session) -> List[AgentTask]:
"""在调用方会话中读取任务列表。"""
return list(session.execute(
_list_for_user_statement(
@@ -117,7 +120,7 @@ class AgentTaskOper(DbOper):
def update(
self,
task_id: int,
payload: dict,
payload: dict[str, Any],
user_id: Optional[str] = None,
) -> bool:
"""
@@ -217,7 +220,7 @@ class AgentTaskOper(DbOper):
task_id: int,
user_id: Optional[str] = None,
limit: int = 10,
) -> list[AgentTaskRun]:
) -> List[AgentTaskRun]:
"""查询任务最近的有界运行历史。"""
return self._execute_sync_query(
lambda session: AgentTaskRun.list_for_task(
@@ -330,7 +333,7 @@ class AgentTaskOper(DbOper):
task: AgentTask,
next_run_at: Optional[str] = None,
timezone: Optional[str] = None,
) -> dict:
) -> dict[str, Any]:
"""
Agent 定时任务转换为工具可返回的结构
"""
@@ -354,7 +357,7 @@ class AgentTaskOper(DbOper):
}
@staticmethod
def run_to_dict(run: AgentTaskRun) -> dict:
def run_to_dict(run: AgentTaskRun) -> dict[str, Any]:
"""将一次 Agent 任务运行转换为工具返回结构。"""
return {
"run_id": run.run_id,
+8 -7
View File
@@ -23,7 +23,8 @@ import app.db.engine as engine_module
from app.db.engine import (_async_pool_enabled, _get_database_engine,
_database_backend_label, get_engine,
get_global_async_engine)
from app.runtime.config import global_vars, settings
from app.runtime.config import global_vars
from app.runtime.settings import get_runtime_setting
from app.runtime.log import logger
from app.runtime.observability import record_metric
@@ -134,7 +135,7 @@ _pooled_async_engines: Dict[int, Any] = {}
_pooled_async_lock = threading.Lock()
# 回退路径(未池化的临时循环)共享的全局连接配额。用 threading 信号量而非
# asyncio.Semaphore:后者绑定单个事件循环,无法跨循环生效
_fallback_slots = threading.BoundedSemaphore(max(1, settings.DB_ASYNC_FALLBACK_LIMIT))
_fallback_slots = threading.BoundedSemaphore(max(1, get_runtime_setting('DB_ASYNC_FALLBACK_LIMIT')))
def _pooled_loop() -> Optional[Any]:
@@ -179,8 +180,8 @@ def _resolve_async_engine() -> Tuple[SaAsyncEngine, bool]:
if engine is None:
engine = cast(SaAsyncEngine, _get_database_engine(is_async=True, pooled=True))
_pooled_async_engines[key] = engine
logger.info(f"异步数据库连接池已启用: pool_size={settings.DB_ASYNC_POOL_SIZE}, "
f"max_overflow={settings.DB_ASYNC_MAX_OVERFLOW}")
logger.info(f"异步数据库连接池已启用: pool_size={get_runtime_setting('DB_ASYNC_POOL_SIZE')}, "
f"max_overflow={get_runtime_setting('DB_ASYNC_MAX_OVERFLOW')}")
return engine, True
@@ -201,7 +202,7 @@ async def _acquire_fallback_slot():
因此用非阻塞获取 + 异步让出
"""
started_at = time.monotonic()
deadline = started_at + settings.DB_POOL_TIMEOUT
deadline = started_at + get_runtime_setting('DB_POOL_TIMEOUT')
outcome = "success"
try:
while not _fallback_slots.acquire(blocking=False):
@@ -212,8 +213,8 @@ async def _acquire_fallback_slot():
backend=_database_backend_label(),
)
raise TimeoutError(
f"异步数据库连接配额已耗尽(上限 {settings.DB_ASYNC_FALLBACK_LIMIT}),"
f"等待超过 {settings.DB_POOL_TIMEOUT}"
f"异步数据库连接配额已耗尽(上限 {get_runtime_setting('DB_ASYNC_FALLBACK_LIMIT')}),"
f"等待超过 {get_runtime_setting('DB_POOL_TIMEOUT')}"
)
await asyncio.sleep(0.01)
finally:
+46 -49
View File
@@ -20,13 +20,10 @@ import psutil
from app.adapters.system.backup.database import verify_database_backup
from app.adapters.system.backup.files import BackupFiles
from app.runtime.settings import RuntimeSettingsCompat
settings = RuntimeSettingsCompat()
from app.runtime.topology import process_topology_issue
from app.doctor.models import DoctorFinding, DoctorFindingStatus, DoctorReport, DoctorSeverity
from app.adapters.system.host import SystemUtils
from app.doctor.models import DoctorFinding, DoctorFindingStatus, DoctorReport, DoctorSeverity
from app.runtime.settings import get_runtime_setting, update_runtime_setting
from app.runtime.topology import process_topology_issue
CheckFunc = Callable[["DoctorRunnerProtocol"], None]
@@ -72,23 +69,23 @@ SENSITIVE_PATTERNS = (
def _backend_runtime_file() -> Path:
return settings.TEMP_PATH / "moviepilot.runtime.json"
return get_runtime_setting('TEMP_PATH') / "moviepilot.runtime.json"
def _frontend_runtime_file() -> Path:
return settings.TEMP_PATH / "moviepilot.frontend.runtime.json"
return get_runtime_setting('TEMP_PATH') / "moviepilot.frontend.runtime.json"
def _backend_stdio_log_file() -> Path:
return settings.LOG_PATH / "moviepilot.stdout.log"
return get_runtime_setting('LOG_PATH') / "moviepilot.stdout.log"
def _backend_app_log_file() -> Path:
return settings.LOG_PATH / "moviepilot.log"
return get_runtime_setting('LOG_PATH') / "moviepilot.log"
def _frontend_stdio_log_file() -> Path:
return settings.LOG_PATH / "moviepilot.frontend.stdout.log"
return get_runtime_setting('LOG_PATH') / "moviepilot.frontend.stdout.log"
class DoctorRunnerProtocol:
@@ -163,12 +160,12 @@ def _mask_text(text: str) -> str:
def _check_process_topology(runner: DoctorRunnerProtocol) -> None:
"""诊断 API worker 配置是否会复制全功能控制面。"""
issue = process_topology_issue(
workers=settings.API_WORKERS,
safe_mode=settings.MOVIEPILOT_SAFE_MODE,
workers=get_runtime_setting('API_WORKERS'),
safe_mode=get_runtime_setting('MOVIEPILOT_SAFE_MODE'),
)
context = {
"api_workers": settings.API_WORKERS,
"safe_mode": settings.MOVIEPILOT_SAFE_MODE,
"api_workers": get_runtime_setting('API_WORKERS'),
"safe_mode": get_runtime_setting('MOVIEPILOT_SAFE_MODE'),
}
if issue:
runner.add(
@@ -181,7 +178,7 @@ def _check_process_topology(runner: DoctorRunnerProtocol) -> None:
context=context,
)
return
if settings.API_WORKERS != 1:
if get_runtime_setting('API_WORKERS') != 1:
runner.add(
finding_id="startup.process_topology",
severity=DoctorSeverity.Warn,
@@ -319,7 +316,7 @@ def _backend_health_payload(port: int, timeout: float = BACKEND_HEALTH_TIMEOUT)
读取本机后端健康接口响应用于识别非 CLI 管理的 MoviePilot 进程
"""
query = urlencode({"token": BACKEND_HEALTH_TOKEN})
url = f"http://{_client_host(settings.HOST)}:{port}{BACKEND_HEALTH_PATH}?{query}"
url = f"http://{_client_host(get_runtime_setting('HOST'))}:{port}{BACKEND_HEALTH_PATH}?{query}"
request = Request(url=url, headers={"Accept": "application/json"}, method="GET")
try:
with urlopen(request, timeout=timeout) as response:
@@ -465,13 +462,13 @@ def _partition_error_lines(
def _frontend_dir() -> Path:
root_public = settings.ROOT_PATH / "public"
configured = Path(settings.FRONTEND_PATH)
root_public = get_runtime_setting('ROOT_PATH') / "public"
configured = Path(get_runtime_setting('FRONTEND_PATH'))
if root_public.exists():
return root_public
if configured.is_absolute():
return configured
return settings.ROOT_PATH / configured
return get_runtime_setting('ROOT_PATH') / configured
def _unlink_if_requested(runner: DoctorRunnerProtocol, path: Path) -> bool:
@@ -491,26 +488,26 @@ def _check_runtime_paths(runner: DoctorRunnerProtocol) -> None:
status=DoctorFindingStatus.Ok,
title="运行路径已识别",
detail=(
f"程序目录:{settings.ROOT_PATH};配置目录:{settings.CONFIG_PATH}"
f"日志目录:{settings.LOG_PATH}Python{sys.executable}"
f"程序目录:{get_runtime_setting('ROOT_PATH')};配置目录:{get_runtime_setting('CONFIG_PATH')}"
f"日志目录:{get_runtime_setting('LOG_PATH')}Python{sys.executable}"
),
recommendation="如需切换配置目录,请使用 CONFIG_DIR 或本地 CLI 的 --config-dir 参数。",
context={
"root_path": str(settings.ROOT_PATH),
"config_path": str(settings.CONFIG_PATH),
"log_path": str(settings.LOG_PATH),
"root_path": str(get_runtime_setting('ROOT_PATH')),
"config_path": str(get_runtime_setting('CONFIG_PATH')),
"log_path": str(get_runtime_setting('LOG_PATH')),
"python": sys.executable,
},
)
def _check_config(runner: DoctorRunnerProtocol) -> None:
token = (settings.API_TOKEN or "").strip()
token = (get_runtime_setting('API_TOKEN') or "").strip()
if len(token) < 16:
fixed = False
detail = "API_TOKEN 未设置或长度小于 16 个字符,后端鉴权和本地工具调用可能不可用。"
if runner.fix and "API_TOKEN" not in os.environ:
result, message = settings.update_setting("API_TOKEN", token)
result, message = update_runtime_setting("API_TOKEN", token)
fixed = result is True
if message:
detail = f"{detail} {message}"
@@ -537,13 +534,13 @@ def _check_config(runner: DoctorRunnerProtocol) -> None:
recommendation="无需处理。",
)
if settings.PORT == settings.NGINX_PORT:
if get_runtime_setting('PORT') == get_runtime_setting('NGINX_PORT'):
runner.add(
finding_id="config.port_same",
severity=DoctorSeverity.Error,
status=DoctorFindingStatus.Failed,
title="前后端端口冲突",
detail=f"PORT 与 NGINX_PORT 都设置为 {settings.PORT}",
detail=f"PORT 与 NGINX_PORT 都设置为 {get_runtime_setting('PORT')}",
recommendation="将 PORT 或 NGINX_PORT 调整为不同端口后重启服务。",
)
else:
@@ -552,11 +549,11 @@ def _check_config(runner: DoctorRunnerProtocol) -> None:
severity=DoctorSeverity.Info,
status=DoctorFindingStatus.Ok,
title="前后端端口配置不同",
detail=f"后端端口 PORT={settings.PORT};前端端口 NGINX_PORT={settings.NGINX_PORT}",
detail=f"后端端口 PORT={get_runtime_setting('PORT')};前端端口 NGINX_PORT={get_runtime_setting('NGINX_PORT')}",
recommendation="无需处理。",
)
proxy_host = (settings.PROXY_HOST or "").strip()
proxy_host = (get_runtime_setting('PROXY_HOST') or "").strip()
if proxy_host and not re.match(r"^(https?|socks5h?)://", proxy_host, re.IGNORECASE):
runner.add(
finding_id="config.proxy_format",
@@ -683,16 +680,16 @@ def _check_processes_and_ports(runner: DoctorRunnerProtocol) -> None:
runner,
name="backend",
path=_backend_runtime_file(),
port=int(settings.PORT),
port=int(get_runtime_setting('PORT')),
)
frontend_process = _check_runtime_file(
runner,
name="frontend",
path=_frontend_runtime_file(),
port=int(settings.NGINX_PORT),
port=int(get_runtime_setting('NGINX_PORT')),
)
_check_port(runner, name="backend", port=int(settings.PORT), managed_process=backend_process)
_check_port(runner, name="frontend", port=int(settings.NGINX_PORT), managed_process=frontend_process)
_check_port(runner, name="backend", port=int(get_runtime_setting('PORT')), managed_process=backend_process)
_check_port(runner, name="frontend", port=int(get_runtime_setting('NGINX_PORT')), managed_process=frontend_process)
def _check_dependencies(runner: DoctorRunnerProtocol) -> None:
@@ -719,7 +716,7 @@ def _check_dependencies(runner: DoctorRunnerProtocol) -> None:
def _check_sqlite_database(runner: DoctorRunnerProtocol) -> None:
db_file = settings.CONFIG_PATH / "user.db"
db_file = get_runtime_setting('CONFIG_PATH') / "user.db"
if not db_file.exists():
runner.add(
finding_id="database.sqlite_missing",
@@ -775,7 +772,7 @@ def _check_sqlite_database(runner: DoctorRunnerProtocol) -> None:
def _check_postgresql_database(runner: DoctorRunnerProtocol) -> None:
missing = []
for key in ("DB_POSTGRESQL_HOST", "DB_POSTGRESQL_DATABASE", "DB_POSTGRESQL_USERNAME"):
if not str(getattr(settings, key, "") or "").strip():
if not str(get_runtime_setting(key, "") or "").strip():
missing.append(key)
if missing:
runner.add(
@@ -800,9 +797,9 @@ def _check_postgresql_database(runner: DoctorRunnerProtocol) -> None:
)
return
host = settings.DB_POSTGRESQL_HOST
port = settings.DB_POSTGRESQL_PORT
if settings.DB_POSTGRESQL_SOCKET_MODE or not port:
host = get_runtime_setting('DB_POSTGRESQL_HOST')
port = get_runtime_setting('DB_POSTGRESQL_PORT')
if get_runtime_setting('DB_POSTGRESQL_SOCKET_MODE') or not port:
runner.add(
finding_id="database.postgresql_deep_skipped",
severity=DoctorSeverity.Info,
@@ -818,13 +815,13 @@ def _check_postgresql_database(runner: DoctorRunnerProtocol) -> None:
severity=DoctorSeverity.Info if ok else DoctorSeverity.Error,
status=DoctorFindingStatus.Ok if ok else DoctorFindingStatus.Failed,
title="PostgreSQL TCP 端口可连接" if ok else "PostgreSQL TCP 端口不可连接",
detail=f"{settings.DB_POSTGRESQL_TARGET} {detail}".strip(),
detail=f"{get_runtime_setting('DB_POSTGRESQL_TARGET')} {detail}".strip(),
recommendation="不可连接时请检查数据库服务、容器网络、端口映射和防火墙。",
)
def _check_database(runner: DoctorRunnerProtocol) -> None:
if settings.DB_TYPE.lower() == "postgresql":
if get_runtime_setting('DB_TYPE').lower() == "postgresql":
_check_postgresql_database(runner)
else:
_check_sqlite_database(runner)
@@ -833,9 +830,9 @@ def _check_database(runner: DoctorRunnerProtocol) -> None:
def _check_database_backups(runner: DoctorRunnerProtocol) -> None:
"""列举并离线校验与当前数据库类型匹配的受管备份。"""
db_type = "postgresql" if settings.DB_TYPE.lower() == "postgresql" else "sqlite"
db_type = "postgresql" if get_runtime_setting('DB_TYPE').lower() == "postgresql" else "sqlite"
try:
paths = BackupFiles(settings.DATABASE_BACKUP_PATH).list()
paths = BackupFiles(get_runtime_setting('DATABASE_BACKUP_PATH')).list()
except OSError as error:
runner.add(
finding_id="database.backup_recovery",
@@ -985,7 +982,7 @@ def _check_logs(runner: DoctorRunnerProtocol) -> None:
_backend_stdio_log_file(),
_frontend_stdio_log_file(),
]
plugin_log_dir = settings.LOG_PATH / "plugins"
plugin_log_dir = get_runtime_setting('LOG_PATH') / "plugins"
plugin_logger_names: set[str] = set()
if plugin_log_dir.exists():
plugin_log_files = sorted(plugin_log_dir.rglob("*.log"))
@@ -1021,7 +1018,7 @@ def _check_logs(runner: DoctorRunnerProtocol) -> None:
severity=DoctorSeverity.Warn,
status=DoctorFindingStatus.Degraded,
title="未找到运行日志",
detail=f"{settings.LOG_PATH} 下没有可读取的 MoviePilot 日志。",
detail=f"{get_runtime_setting('LOG_PATH')} 下没有可读取的 MoviePilot 日志。",
recommendation="如果服务尚未启动过可忽略;否则请确认 CONFIG_DIR 和日志目录权限。",
)
return
@@ -1067,7 +1064,7 @@ def _check_logs(runner: DoctorRunnerProtocol) -> None:
status=DoctorFindingStatus.Ok,
title="最近日志未发现明显错误关键词",
detail=(
f"已扫描 {settings.LOG_PATH} 下最近 {LOG_LOOKBACK_HOURS} 小时的主日志、"
f"已扫描 {get_runtime_setting('LOG_PATH')} 下最近 {LOG_LOOKBACK_HOURS} 小时的主日志、"
"启动日志和插件日志;插件扩展告警不参与核心健康状态。"
),
recommendation="如果问题仍存在,请结合具体操作时间扩大日志范围排查。",
@@ -1113,7 +1110,7 @@ def _check_docker(runner: DoctorRunnerProtocol) -> None:
status=DoctorFindingStatus.Ok,
title="Docker 诊断入口可用",
detail=(
f"CONFIG_DIR={settings.CONFIG_PATH}VENV_PATH={os.getenv('VENV_PATH', '/opt/venv')}"
f"CONFIG_DIR={get_runtime_setting('CONFIG_PATH')}VENV_PATH={os.getenv('VENV_PATH', '/opt/venv')}"
f"MOVIEPILOT_DOCKER_KEEPALIVE_ON_FAILURE={os.getenv('MOVIEPILOT_DOCKER_KEEPALIVE_ON_FAILURE', 'true')}"
),
recommendation="主进程异常退出后容器会保活,仍可通过 `docker exec <container> moviepilot doctor` 诊断。",
@@ -1121,7 +1118,7 @@ def _check_docker(runner: DoctorRunnerProtocol) -> None:
def _check_safe_mode(runner: DoctorRunnerProtocol) -> None:
if settings.MOVIEPILOT_SAFE_MODE:
if get_runtime_setting('MOVIEPILOT_SAFE_MODE'):
runner.add(
finding_id="startup.safe_mode",
severity=DoctorSeverity.Warn,
+6 -7
View File
@@ -6,9 +6,8 @@ import sys
from datetime import datetime
from typing import Any, Optional
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.doctor.checks import default_checks
from app.doctor.models import (
DoctorFinding,
@@ -110,12 +109,12 @@ class DoctorRunner:
"platform": platform.platform(),
"python": sys.executable,
"python_version": platform.python_version(),
"root_path": str(settings.ROOT_PATH),
"config_path": str(settings.CONFIG_PATH),
"log_path": str(settings.LOG_PATH),
"temp_path": str(settings.TEMP_PATH),
"root_path": str(get_runtime_setting('ROOT_PATH')),
"config_path": str(get_runtime_setting('CONFIG_PATH')),
"log_path": str(get_runtime_setting('LOG_PATH')),
"temp_path": str(get_runtime_setting('TEMP_PATH')),
"is_docker": SystemUtils.is_docker(),
"safe_mode": settings.MOVIEPILOT_SAFE_MODE,
"safe_mode": get_runtime_setting('MOVIEPILOT_SAFE_MODE'),
"pid": os.getpid(),
}
+22 -23
View File
@@ -7,41 +7,40 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from starlette.exceptions import HTTPException
from app.api.response import ResponseAPIRoute
from app.adapters.web.correlation import CorrelationIdMiddleware
from app.adapters.web.metrics import HttpMetricsMiddleware
from app.adapters.observability.otel import build_observation_port
from app.adapters.web.plugin.routes import FastAPIDynamicRouteRegistry
from app.adapters.web.correlation import CorrelationIdMiddleware
from app.adapters.web.health import install_health_routes
from app.application.plugin.routes import configure_plugin_routes
from app.application.plugin.runtime import get_plugin_manager
from app.schemas.exception import (
PersistenceUnavailableError,
)
from app.adapters.web.metrics import HttpMetricsMiddleware
from app.adapters.web.plugin.routes import FastAPIDynamicRouteRegistry
from app.adapters.web.security.access import (
configure_token_codec,
verify_apikey,
verify_token,
)
from app.api.response import ResponseAPIRoute
from app.application.plugin.routes import configure_plugin_routes
from app.application.plugin.runtime import get_plugin_manager
from app.application.security.token import create_access_token, decode_access_token
from app.runtime.config import global_vars
from app.runtime.settings import RuntimeSettingsCompat
settings = RuntimeSettingsCompat()
from app.runtime.correlation import get_correlation_id
from app.runtime.localization import LocaleHelper
from app.runtime.log import configure_correlation_id_provider, logger
from app.runtime.observability import configure_observation
from app.runtime.settings import get_runtime_setting
from app.runtime.version import get_app_version
from app.schemas.exception import (
PersistenceUnavailableError,
)
from app.schemas.mcp import McpJsonRpcError, McpJsonRpcErrorDetail
from app.schemas.openai import (
AnthropicErrorDetail,
AnthropicErrorResponse,
OpenAIErrorDetail,
OpenAIErrorResponse,
)
from app.schemas.mcp import McpJsonRpcError, McpJsonRpcErrorDetail
from app.schemas.response import Response as ApiResponse, ValidationIssue
from app.schemas.response import Response as ApiResponse
from app.schemas.response import ValidationIssue
from app.startup.lifecycle import lifespan
from app.runtime.version import get_app_version
def _get_http_exception_message(detail: Any) -> str:
@@ -67,15 +66,15 @@ def _localize_exception_message(request: Request, message: str) -> str:
def _is_mcp_jsonrpc_request(request: Request) -> bool:
"""判断请求是否指向保持原生响应的 MCP JSON-RPC 根端点。"""
request_path = getattr(getattr(request, "url", None), "path", "")
return request_path.rstrip("/") == f"{settings.API_V1_STR}/mcp"
return request_path.rstrip("/") == f"{get_runtime_setting('API_V1_STR')}/mcp"
def _get_native_ai_protocol(request: Request) -> str | None:
"""识别需要保持原生错误体的 OpenAI 或 Anthropic 兼容请求。"""
request_path = getattr(getattr(request, "url", None), "path", "")
if request_path.startswith(f"{settings.API_V1_STR}/openai/v1/"):
if request_path.startswith(f"{get_runtime_setting('API_V1_STR')}/openai/v1/"):
return "openai"
if request_path.startswith(f"{settings.API_V1_STR}/anthropic/v1/"):
if request_path.startswith(f"{get_runtime_setting('API_V1_STR')}/anthropic/v1/"):
return "anthropic"
return None
@@ -327,9 +326,9 @@ def create_app() -> FastAPI:
configure_correlation_id_provider(get_correlation_id)
configure_observation(build_observation_port())
_app = FastAPI(
title=settings.PROJECT_NAME,
title=get_runtime_setting('PROJECT_NAME'),
version=get_app_version(),
openapi_url=f"{settings.API_V1_STR}/openapi.json",
openapi_url=f"{get_runtime_setting('API_V1_STR')}/openapi.json",
lifespan=lifespan
)
@@ -351,7 +350,7 @@ def create_app() -> FastAPI:
# 配置 CORS 中间件
_app.add_middleware(
CORSMiddleware, # noqa
allow_origins=settings.ALLOWED_HOSTS,
allow_origins=get_runtime_setting('ALLOWED_HOSTS'),
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
@@ -386,9 +385,9 @@ def create_app() -> FastAPI:
plugin_apis=lambda plugin_id: get_plugin_manager().get_plugin_apis(plugin_id),
verify_token=verify_token,
verify_apikey=verify_apikey,
prefix=f"{settings.API_V1_STR}/plugin",
prefix=f"{get_runtime_setting('API_V1_STR')}/plugin",
protected_routes={
f"{settings.API_V1_STR}/openapi.json",
f"{get_runtime_setting('API_V1_STR')}/openapi.json",
"/docs",
"/docs/oauth2-redirect",
"/redoc",
+15 -16
View File
@@ -55,17 +55,16 @@ elif SystemUtils.is_frozen():
sys.stderr = open(os.devnull, 'w')
from app.factory import app
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
from app.runtime.config import global_vars
from app.runtime.stop import runtime_stop_state
settings = RuntimeSettingsCompat()
from app.runtime.topology import (
UnsupportedProcessTopologyError,
validate_process_topology,
)
setproctitle.setproctitle(settings.PROJECT_NAME)
setproctitle.setproctitle(get_runtime_setting('PROJECT_NAME'))
class MoviePilotServer(uvicorn.Server):
@@ -85,8 +84,8 @@ def create_server() -> MoviePilotServer:
server = MoviePilotServer(
Config(
app,
host=settings.HOST,
port=settings.PORT,
host=get_runtime_setting('HOST'),
port=get_runtime_setting('PORT'),
reload=False,
workers=1,
timeout_graceful_shutdown=60,
@@ -101,9 +100,9 @@ def create_server() -> MoviePilotServer:
def run_api_server() -> None:
"""按开发 reload、安全模式多进程或生产单进程选择 Uvicorn 入口。"""
global Server
supervised = settings.DEV or settings.API_WORKERS > 1
supervised = get_runtime_setting('DEV') or get_runtime_setting('API_WORKERS') > 1
if supervised:
if settings.DEV and settings.API_WORKERS > 1:
if get_runtime_setting('DEV') and get_runtime_setting('API_WORKERS') > 1:
raise UnsupportedProcessTopologyError(
"Uvicorn reload 与多 worker 不能同时启用;"
"开发模式请设置 API_WORKERS=1。"
@@ -112,10 +111,10 @@ def run_api_server() -> None:
uvicorn.run(
APP_FACTORY,
factory=True,
host=settings.HOST,
port=settings.PORT,
reload=settings.DEV,
workers=settings.API_WORKERS,
host=get_runtime_setting('HOST'),
port=get_runtime_setting('PORT'),
reload=get_runtime_setting('DEV'),
workers=get_runtime_setting('API_WORKERS'),
timeout_graceful_shutdown=60,
)
return
@@ -146,7 +145,7 @@ def start_tray():
调用浏览器打开前端页面
"""
import webbrowser
webbrowser.open(f"http://localhost:{settings.NGINX_PORT}")
webbrowser.open(f"http://localhost:{get_runtime_setting('NGINX_PORT')}")
def quit_app():
"""
@@ -158,8 +157,8 @@ def start_tray():
import pystray
TrayIcon = pystray.Icon(
settings.PROJECT_NAME,
icon=Image.open(settings.ROOT_PATH / 'app.ico'),
get_runtime_setting('PROJECT_NAME'),
icon=Image.open(get_runtime_setting('ROOT_PATH') / 'app.ico'),
menu=pystray.Menu(
pystray.MenuItem(
'打开',
@@ -185,8 +184,8 @@ def signal_handler(signum, frame):
def run_application() -> None:
"""初始化进程并启动 API 服务"""
validate_process_topology(
workers=settings.API_WORKERS,
safe_mode=settings.MOVIEPILOT_SAFE_MODE,
workers=get_runtime_setting('API_WORKERS'),
safe_mode=get_runtime_setting('MOVIEPILOT_SAFE_MODE'),
)
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
+11 -12
View File
@@ -10,9 +10,8 @@ from typing import Any, Optional, Tuple, Union
from uuid import UUID
from app.runtime.execution import run_in_threadpool
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.modules import _ModuleBase
from app.schemas.types import ModuleType, OtherModulesType
@@ -71,19 +70,19 @@ class AcoustIdModule(_ModuleBase):
模块初始化早于 fpcalc 安装或运行期依赖被移除测试也能如实反映本地
依赖状态而不是只校验网络连通性
"""
if not str(settings.ACOUSTID_API_KEY or "").strip():
if not str(get_runtime_setting('ACOUSTID_API_KEY') or "").strip():
return False, "AcoustID API Key 未配置"
fpcalc_path = self._resolve_fpcalc()
if not fpcalc_path:
return False, "未找到 fpcalc,请先安装 Chromaprint"
self._fpcalc_path = fpcalc_path
response = RequestUtils(
ua=settings.USER_AGENT,
proxies=settings.PROXY,
ua=get_runtime_setting('USER_AGENT'),
proxies=get_runtime_setting('PROXY'),
timeout=15,
).get_res(
url=self._base_url,
params={"client": settings.ACOUSTID_API_KEY, "format": "json"},
params={"client": get_runtime_setting('ACOUSTID_API_KEY'), "format": "json"},
)
if response is None:
return False, "AcoustID 网络连接失败"
@@ -298,13 +297,13 @@ class AcoustIdModule(_ModuleBase):
fingerprint: str,
) -> Optional[str]:
"""查询 AcoustID 指纹库并提取 MusicBrainz Recording ID。"""
api_key = str(settings.ACOUSTID_API_KEY or "").strip()
api_key = str(get_runtime_setting('ACOUSTID_API_KEY') or "").strip()
if not api_key:
return None
self._wait_for_rate_limit()
response = RequestUtils(
ua=settings.USER_AGENT,
proxies=settings.PROXY,
ua=get_runtime_setting('USER_AGENT'),
proxies=get_runtime_setting('PROXY'),
timeout=30,
).post_res(
url=self._base_url,
@@ -337,13 +336,13 @@ class AcoustIdModule(_ModuleBase):
fingerprint: str,
) -> Optional[str]:
"""异步查询 AcoustID 指纹库并提取 MusicBrainz Recording ID。"""
api_key = str(settings.ACOUSTID_API_KEY or "").strip()
api_key = str(get_runtime_setting('ACOUSTID_API_KEY') or "").strip()
if not api_key:
return None
await self._async_wait_for_rate_limit()
response = await AsyncRequestUtils(
ua=settings.USER_AGENT,
proxies=settings.PROXY,
ua=get_runtime_setting('USER_AGENT'),
proxies=get_runtime_setting('PROXY'),
timeout=30,
).post_res(
url=self._base_url,
+3 -3
View File
@@ -77,7 +77,7 @@ class AniListModule(MediaAuxiliaryProviderMixin, _ModuleBase):
:param media_source: 请求级识别数据源
:return: 是否启用 AniList 识别
"""
return (media_source or get_runtime_setting("RECOGNIZE_SOURCE")) == MediaSource.AniList
return (media_source or get_runtime_setting('RECOGNIZE_SOURCE')) == MediaSource.AniList
@staticmethod
def _media_type(info: dict) -> MediaType:
@@ -570,7 +570,7 @@ class AniListModule(MediaAuxiliaryProviderMixin, _ModuleBase):
:param episode: 集号
:return: NFO XML 文本
"""
scrape_source = mediainfo.scrape_source or get_runtime_setting("SCRAP_SOURCE")
scrape_source = mediainfo.scrape_source or get_runtime_setting('SCRAP_SOURCE')
if scrape_source != "anilist":
return None
return self.scraper.get_metadata_nfo(mediainfo, season=season, episode=episode)
@@ -589,7 +589,7 @@ class AniListModule(MediaAuxiliaryProviderMixin, _ModuleBase):
:param episode: 集号
:return: 图片文件名与下载地址映射
"""
scrape_source = mediainfo.scrape_source or get_runtime_setting("SCRAP_SOURCE")
scrape_source = mediainfo.scrape_source or get_runtime_setting('SCRAP_SOURCE')
if scrape_source != "anilist":
return None
return self.scraper.get_metadata_img(mediainfo, season=season, episode=episode)
+32 -33
View File
@@ -2,9 +2,8 @@ from datetime import date
from typing import Optional
from app.runtime.cache import cached
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.adapters.network.http import AsyncRequestUtils, RequestUtils
@@ -103,16 +102,16 @@ class AniListApi:
def __init__(self) -> None:
"""初始化同步与异步请求客户端"""
headers = {
"User-Agent": settings.NORMAL_USER_AGENT,
"User-Agent": get_runtime_setting('NORMAL_USER_AGENT'),
"Accept": "application/json",
"Content-Type": "application/json",
}
self._request = RequestUtils(
proxies=settings.PROXY,
proxies=get_runtime_setting('PROXY'),
headers=headers,
)
self._async_request = AsyncRequestUtils(
proxies=settings.PROXY,
proxies=get_runtime_setting('PROXY'),
headers=headers,
)
self._proxy_available = True
@@ -363,8 +362,8 @@ class AniListApi:
return seasons[(current.month - 1) // 3], current.year
@cached(
maxsize=settings.CONF.anilist,
ttl=settings.CONF.meta,
maxsize=get_runtime_setting('CONF').anilist,
ttl=get_runtime_setting('CONF').meta,
skip_empty=True,
shared_key="detail",
)
@@ -380,8 +379,8 @@ class AniListApi:
return result.get("Media") if result else None
@cached(
maxsize=settings.CONF.anilist,
ttl=settings.CONF.meta,
maxsize=get_runtime_setting('CONF').anilist,
ttl=get_runtime_setting('CONF').meta,
skip_empty=True,
shared_key="detail",
)
@@ -397,8 +396,8 @@ class AniListApi:
return result.get("Media") if result else None
@cached(
maxsize=settings.CONF.anilist,
ttl=settings.CONF.meta,
maxsize=get_runtime_setting('CONF').anilist,
ttl=get_runtime_setting('CONF').meta,
skip_empty=True,
shared_key="search",
)
@@ -421,8 +420,8 @@ class AniListApi:
return self._page_medias(result)
@cached(
maxsize=settings.CONF.anilist,
ttl=settings.CONF.meta,
maxsize=get_runtime_setting('CONF').anilist,
ttl=get_runtime_setting('CONF').meta,
skip_empty=True,
shared_key="search",
)
@@ -445,8 +444,8 @@ class AniListApi:
return self._page_medias(result)
@cached(
maxsize=settings.CONF.anilist,
ttl=settings.CONF.meta,
maxsize=get_runtime_setting('CONF').anilist,
ttl=get_runtime_setting('CONF').meta,
skip_empty=True,
shared_key="discover",
)
@@ -483,8 +482,8 @@ class AniListApi:
return self._page_medias(self._invoke(self._page_query, variables))
@cached(
maxsize=settings.CONF.anilist,
ttl=settings.CONF.meta,
maxsize=get_runtime_setting('CONF').anilist,
ttl=get_runtime_setting('CONF').meta,
skip_empty=True,
shared_key="discover",
)
@@ -576,8 +575,8 @@ class AniListApi:
)
@cached(
maxsize=settings.CONF.anilist,
ttl=settings.CONF.meta,
maxsize=get_runtime_setting('CONF').anilist,
ttl=get_runtime_setting('CONF').meta,
skip_empty=True,
shared_key="credits",
)
@@ -606,8 +605,8 @@ class AniListApi:
return result.get("Media", {}).get("characters", {}).get("edges") or [] if result else []
@cached(
maxsize=settings.CONF.anilist,
ttl=settings.CONF.meta,
maxsize=get_runtime_setting('CONF').anilist,
ttl=get_runtime_setting('CONF').meta,
skip_empty=True,
shared_key="credits",
)
@@ -636,8 +635,8 @@ class AniListApi:
return result.get("Media", {}).get("characters", {}).get("edges") or [] if result else []
@cached(
maxsize=settings.CONF.anilist,
ttl=settings.CONF.meta,
maxsize=get_runtime_setting('CONF').anilist,
ttl=get_runtime_setting('CONF').meta,
skip_empty=True,
shared_key="recommendations",
)
@@ -662,8 +661,8 @@ class AniListApi:
return self._medias_by_ids(media_ids)
@cached(
maxsize=settings.CONF.anilist,
ttl=settings.CONF.meta,
maxsize=get_runtime_setting('CONF').anilist,
ttl=get_runtime_setting('CONF').meta,
skip_empty=True,
shared_key="recommendations",
)
@@ -688,8 +687,8 @@ class AniListApi:
return await self._async_medias_by_ids(media_ids)
@cached(
maxsize=settings.CONF.anilist,
ttl=settings.CONF.meta,
maxsize=get_runtime_setting('CONF').anilist,
ttl=get_runtime_setting('CONF').meta,
skip_empty=True,
shared_key="person_detail",
)
@@ -713,8 +712,8 @@ class AniListApi:
return result.get("Staff") if result else None
@cached(
maxsize=settings.CONF.anilist,
ttl=settings.CONF.meta,
maxsize=get_runtime_setting('CONF').anilist,
ttl=get_runtime_setting('CONF').meta,
skip_empty=True,
shared_key="person_detail",
)
@@ -738,8 +737,8 @@ class AniListApi:
return result.get("Staff") if result else None
@cached(
maxsize=settings.CONF.anilist,
ttl=settings.CONF.meta,
maxsize=get_runtime_setting('CONF').anilist,
ttl=get_runtime_setting('CONF').meta,
skip_empty=True,
shared_key="person_credits",
)
@@ -763,8 +762,8 @@ class AniListApi:
return self._medias_by_ids([node.get("id") for node in nodes])
@cached(
maxsize=settings.CONF.anilist,
ttl=settings.CONF.meta,
maxsize=get_runtime_setting('CONF').anilist,
ttl=get_runtime_setting('CONF').meta,
skip_empty=True,
shared_key="person_credits",
)
+5 -5
View File
@@ -44,7 +44,7 @@ class BangumiModule(MediaAuxiliaryProviderMixin, _ModuleBase):
"""
初始化Bangumi客户端
"""
self._config = BangumiConfigSnapshot(proxy=get_runtime_setting("PROXY"))
self._config = BangumiConfigSnapshot(proxy=get_runtime_setting('PROXY'))
self.bangumiapi = BangumiApi()
self.scraper = MediaScraperHelper()
@@ -126,7 +126,7 @@ class BangumiModule(MediaAuxiliaryProviderMixin, _ModuleBase):
return None
bangumiid = int(media_id) if media_id is not None else None
if not bangumiid and (
not meta or (media_source or get_runtime_setting("RECOGNIZE_SOURCE")) != MediaSource.Bangumi
not meta or (media_source or get_runtime_setting('RECOGNIZE_SOURCE')) != MediaSource.Bangumi
):
return None
@@ -175,7 +175,7 @@ class BangumiModule(MediaAuxiliaryProviderMixin, _ModuleBase):
return None
bangumiid = int(media_id) if media_id is not None else None
if not bangumiid and (
not meta or (media_source or get_runtime_setting("RECOGNIZE_SOURCE")) != MediaSource.Bangumi
not meta or (media_source or get_runtime_setting('RECOGNIZE_SOURCE')) != MediaSource.Bangumi
):
return None
@@ -316,7 +316,7 @@ class BangumiModule(MediaAuxiliaryProviderMixin, _ModuleBase):
:param episode: 集号
:return: NFO XML文本
"""
scrape_source = mediainfo.scrape_source or get_runtime_setting("SCRAP_SOURCE")
scrape_source = mediainfo.scrape_source or get_runtime_setting('SCRAP_SOURCE')
if scrape_source != "bangumi":
return None
return self.scraper.get_metadata_nfo(mediainfo, season=season, episode=episode)
@@ -335,7 +335,7 @@ class BangumiModule(MediaAuxiliaryProviderMixin, _ModuleBase):
:param episode: 集号
:return: 图片文件名与下载地址映射
"""
scrape_source = mediainfo.scrape_source or get_runtime_setting("SCRAP_SOURCE")
scrape_source = mediainfo.scrape_source or get_runtime_setting('SCRAP_SOURCE')
if scrape_source != "bangumi":
return None
return self.scraper.get_metadata_img(mediainfo, season=season, episode=episode)
+7 -8
View File
@@ -4,9 +4,8 @@ from typing import Optional
import requests
from app.runtime.cache import cached
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.adapters.network.http import RequestUtils, AsyncRequestUtils
@@ -33,16 +32,16 @@ class BangumiApi(object):
def __init__(self):
self._session = requests.Session()
self._req = RequestUtils(
ua=settings.NORMAL_USER_AGENT,
proxies=settings.PROXY,
ua=get_runtime_setting('NORMAL_USER_AGENT'),
proxies=get_runtime_setting('PROXY'),
session=self._session,
)
self._async_req = AsyncRequestUtils(
ua=settings.NORMAL_USER_AGENT,
proxies=settings.PROXY,
ua=get_runtime_setting('NORMAL_USER_AGENT'),
proxies=get_runtime_setting('PROXY'),
)
@cached(maxsize=settings.CONF.bangumi, ttl=settings.CONF.meta, shared_key="get")
@cached(maxsize=get_runtime_setting('CONF').bangumi, ttl=get_runtime_setting('CONF').meta, shared_key="get")
def __invoke(self, url, key: Optional[str] = None, **kwargs):
req_url = self._base_url + url
params = {}
@@ -58,7 +57,7 @@ class BangumiApi(object):
print(e)
return None
@cached(maxsize=settings.CONF.bangumi, ttl=settings.CONF.meta, shared_key="get")
@cached(maxsize=get_runtime_setting('CONF').bangumi, ttl=get_runtime_setting('CONF').meta, shared_key="get")
async def __async_invoke(self, url, key: Optional[str] = None, **kwargs):
req_url = self._base_url + url
params = {}
+2 -3
View File
@@ -8,9 +8,8 @@ import discord
from discord import app_commands
from app.runtime.execution import run_in_threadpool
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.application.messaging.ingress import async_forward_message_to_host
from app.domain.context import MediaInfo, Context
from app.domain.metainfo import MetaInfo
@@ -72,7 +71,7 @@ class Discord:
intents.guilds = True
self._client: Optional[discord.Client] = discord.Client(
intents=intents, proxy=settings.PROXY_HOST
intents=intents, proxy=get_runtime_setting('PROXY_HOST')
)
self._tree: Optional[app_commands.CommandTree] = app_commands.CommandTree(self._client)
self._loop: asyncio.AbstractEventLoop = asyncio.new_event_loop()
+5 -5
View File
@@ -706,7 +706,7 @@ class DoubanModule(MediaAuxiliaryProviderMixin, _ModuleBase):
if (
meta
and not doubanid
and (kwargs.get("media_source") or get_runtime_setting("RECOGNIZE_SOURCE")) != "douban"
and (kwargs.get("media_source") or get_runtime_setting('RECOGNIZE_SOURCE')) != "douban"
):
return None
@@ -777,7 +777,7 @@ class DoubanModule(MediaAuxiliaryProviderMixin, _ModuleBase):
if (
meta
and not doubanid
and (kwargs.get("media_source") or get_runtime_setting("RECOGNIZE_SOURCE")) != "douban"
and (kwargs.get("media_source") or get_runtime_setting('RECOGNIZE_SOURCE')) != "douban"
):
return None
@@ -1684,7 +1684,7 @@ class DoubanModule(MediaAuxiliaryProviderMixin, _ModuleBase):
:param mediainfo: 媒体信息
:param season: 季号
"""
if (mediainfo.scrape_source or get_runtime_setting("SCRAP_SOURCE")) != "douban":
if (mediainfo.scrape_source or get_runtime_setting('SCRAP_SOURCE')) != "douban":
return None
return self.scraper.get_metadata_nfo(mediainfo=mediainfo, season=season)
@@ -1695,7 +1695,7 @@ class DoubanModule(MediaAuxiliaryProviderMixin, _ModuleBase):
:param season: 季号
:param episode: 集号
"""
if (mediainfo.scrape_source or get_runtime_setting("SCRAP_SOURCE")) != "douban":
if (mediainfo.scrape_source or get_runtime_setting('SCRAP_SOURCE')) != "douban":
return None
return self.scraper.get_metadata_img(mediainfo=mediainfo, season=season, episode=episode)
@@ -1706,7 +1706,7 @@ class DoubanModule(MediaAuxiliaryProviderMixin, _ModuleBase):
:param mediainfo: 媒体信息
:return: None 表示不处理MediaInfo 表示继续处理
"""
if mediainfo.media_source != MediaSource.Douban and get_runtime_setting("RECOGNIZE_SOURCE") != "douban":
if mediainfo.media_source != MediaSource.Douban and get_runtime_setting('RECOGNIZE_SOURCE') != "douban":
return None
if not mediainfo.douban_id:
return None
+13 -14
View File
@@ -13,9 +13,8 @@ import requests
from bs4 import BeautifulSoup
from app.runtime.cache import cached
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.adapters.network.http import RequestUtils, AsyncRequestUtils
from app.foundation.singleton import WeakSingleton
@@ -233,7 +232,7 @@ class DoubanApi(metaclass=WeakSingleton):
"""
return resp.json() if resp is not None else None
@cached(maxsize=settings.CONF.douban, ttl=settings.CONF.meta, skip_none=True, shared_key="get")
@cached(maxsize=get_runtime_setting('CONF').douban, ttl=get_runtime_setting('CONF').meta, skip_none=True, shared_key="get")
def __invoke(self, url: str, **kwargs) -> dict:
"""
GET请求
@@ -245,7 +244,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, skip_none=True, shared_key="get")
@cached(maxsize=get_runtime_setting('CONF').douban, ttl=get_runtime_setting('CONF').meta, skip_none=True, shared_key="get")
async def __async_invoke(self, url: str, **kwargs) -> dict:
"""
GET请求异步版本
@@ -268,7 +267,7 @@ class DoubanApi(metaclass=WeakSingleton):
params.pop('_ts')
return req_url, params
@cached(maxsize=settings.CONF.douban, ttl=settings.CONF.meta, skip_none=True, shared_key="post")
@cached(maxsize=get_runtime_setting('CONF').douban, ttl=get_runtime_setting('CONF').meta, skip_none=True, shared_key="post")
def __post(self, url: str, **kwargs) -> dict:
"""
POST请求
@@ -285,19 +284,19 @@ class DoubanApi(metaclass=WeakSingleton):
"""
req_url, params = self._prepare_post_request(url, **kwargs)
resp = RequestUtils(
ua=settings.NORMAL_USER_AGENT,
ua=get_runtime_setting('NORMAL_USER_AGENT'),
session=self._session,
).post_res(url=req_url, data=params)
return self._handle_response(resp)
@cached(maxsize=settings.CONF.douban, ttl=settings.CONF.meta, skip_none=True, shared_key="post")
@cached(maxsize=get_runtime_setting('CONF').douban, ttl=get_runtime_setting('CONF').meta, skip_none=True, shared_key="post")
async def __async_post(self, url: str, **kwargs) -> dict:
"""
POST请求异步版本
"""
req_url, params = self._prepare_post_request(url, **kwargs)
resp = await AsyncRequestUtils(
ua=settings.NORMAL_USER_AGENT
ua=get_runtime_setting('NORMAL_USER_AGENT')
).post_res(url=req_url, data=params)
return self._handle_response(resp)
@@ -644,7 +643,7 @@ class DoubanApi(metaclass=WeakSingleton):
self._urls["music_single"], start=start, count=count
)
@cached(maxsize=settings.CONF.douban, ttl=settings.CONF.meta, skip_none=True)
@cached(maxsize=get_runtime_setting('CONF').douban, ttl=get_runtime_setting('CONF').meta, skip_none=True)
def music_tag(
self,
tag: str,
@@ -665,8 +664,8 @@ class DoubanApi(metaclass=WeakSingleton):
while len(items) < required:
url = f"{self._music_web_url}/tag/{parse.quote(normalized_tag, safe='')}"
response = RequestUtils(
ua=settings.NORMAL_USER_AGENT,
proxies=settings.PROXY,
ua=get_runtime_setting('NORMAL_USER_AGENT'),
proxies=get_runtime_setting('PROXY'),
timeout=20,
accept_type="text/html,application/xhtml+xml",
).get_res(url=url, params={"start": page * page_size, "type": sort})
@@ -679,12 +678,12 @@ class DoubanApi(metaclass=WeakSingleton):
page += 1
return {"items": items[first_offset:first_offset + max(count, 1)]}
@cached(maxsize=settings.CONF.douban, ttl=settings.CONF.meta, skip_none=True)
@cached(maxsize=get_runtime_setting('CONF').douban, ttl=get_runtime_setting('CONF').meta, skip_none=True)
def music_chart(self) -> dict:
"""从豆瓣音乐官方榜单页读取新碟榜,并补充专辑详情供卡片展示。"""
response = RequestUtils(
ua=settings.NORMAL_USER_AGENT,
proxies=settings.PROXY,
ua=get_runtime_setting('NORMAL_USER_AGENT'),
proxies=get_runtime_setting('PROXY'),
timeout=20,
accept_type="text/html,application/xhtml+xml",
).get_res(url=f"{self._music_web_url}/chart")
+2 -3
View File
@@ -12,9 +12,8 @@ from app.schemas.mediaserver import MediaServerLibrary as _SchemaMediaServerLibr
from app.schemas.mediaserver import MediaServerPlayItem as _SchemaMediaServerPlayItem
from app.schemas.mediaserver import RefreshMediaItem as _SchemaRefreshMediaItem
from app.schemas.mediaserver import WebhookEventInfo as _SchemaWebhookEventInfo
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.application.mediaserver import MediaServerIdentityHelper, format_emby_family_item
from app.runtime.log import logger
from app.schemas.mediaserver import MediaServerItem
@@ -44,7 +43,7 @@ class Emby:
self._playhost = UrlUtils.standardize_base_url(self._playhost)
self._apikey = apikey
self._username = username
self.user = self.get_user(username or settings.SUPERUSER)
self.user = self.get_user(username or get_runtime_setting('SUPERUSER'))
self.folders = self.get_emby_folders()
self.serverid = self.get_server_id()
self._sync_libraries = sync_libraries or []
+8 -9
View File
@@ -4,9 +4,8 @@ from typing import Optional, Tuple, Union
from app.runtime.cache import cached
from app.domain.context import MediaInfo
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.runtime.tasks import get_task_registry
from app.modules import _ModuleBase
@@ -309,14 +308,14 @@ class FanartModule(_ModuleBase):
"""
# 代理
_proxies: dict = settings.PROXY
_proxies: dict = get_runtime_setting('PROXY')
# Fanart Api
_movie_url: str = (
f"https://webservice.fanart.tv/v3/movies/%s?api_key={settings.FANART_API_KEY}"
f"https://webservice.fanart.tv/v3/movies/%s?api_key={get_runtime_setting('FANART_API_KEY')}"
)
_tv_url: str = (
f"https://webservice.fanart.tv/v3/tv/%s?api_key={settings.FANART_API_KEY}"
f"https://webservice.fanart.tv/v3/tv/%s?api_key={get_runtime_setting('FANART_API_KEY')}"
)
def init_module(self) -> None:
@@ -451,7 +450,7 @@ class FanartModule(_ModuleBase):
"""
获取 Fanart 查询参数
"""
if not settings.FANART_ENABLE:
if not get_runtime_setting('FANART_ENABLE'):
return None
if not mediainfo.tmdb_id and not mediainfo.tvdb_id:
return None
@@ -532,7 +531,7 @@ class FanartModule(_ModuleBase):
"""
其他图片优先环境变量指定语言再like最多
"""
lang_env = settings.FANART_LANG
lang_env = get_runtime_setting('FANART_LANG')
if lang_env:
langs = [lang.strip() for lang in lang_env.split(",") if lang.strip()]
for lang in langs:
@@ -582,7 +581,7 @@ class FanartModule(_ModuleBase):
return cls._FANART_NAME_MAP.get(fanart_name.lower(), fanart_name)
@classmethod
@cached(maxsize=settings.CONF.fanart, ttl=settings.CONF.meta, shared_key="get")
@cached(maxsize=get_runtime_setting('CONF').fanart, ttl=get_runtime_setting('CONF').meta, shared_key="get")
def __request_fanart(
cls, media_type: MediaType, queryid: Union[str, int]
) -> Optional[dict]:
@@ -601,7 +600,7 @@ class FanartModule(_ModuleBase):
return None
@classmethod
@cached(maxsize=settings.CONF.fanart, ttl=settings.CONF.meta, shared_key="get")
@cached(maxsize=get_runtime_setting('CONF').fanart, ttl=get_runtime_setting('CONF').meta, shared_key="get")
async def __async_request_fanart(
cls, media_type: MediaType, queryid: Union[str, int]
) -> Optional[dict]:
+2 -3
View File
@@ -50,9 +50,8 @@ from lark_oapi.event.callback.model.p2_card_action_trigger import (
P2CardActionTriggerResponse,
)
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.application.messaging.ingress import submit_message_to_host
from app.domain.context import Context, MediaInfo
from app.application.security.user import get_configured_user_channel_lookup
@@ -1065,7 +1064,7 @@ class Feishu:
response = None
temp_path = None
try:
response = RequestUtils(timeout=30, ua=settings.USER_AGENT).get_res(image_url)
response = RequestUtils(timeout=30, ua=get_runtime_setting('USER_AGENT')).get_res(image_url)
if not response or not getattr(response, "content", None):
logger.warning(f"飞书图片下载失败:{image_url}")
return None
+6 -7
View File
@@ -1,9 +1,8 @@
from pathlib import Path
from typing import Any, Optional, List, Tuple, Union, Dict, Callable
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.domain.context import MediaInfo, MusicInfo
from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic
@@ -191,7 +190,7 @@ class FileManagerModule(_ModuleBase):
"""
handler = TransHandler()
# 重命名格式
rename_format = settings.RENAME_FORMAT(mediainfo.type)
rename_format = get_runtime_setting('RENAME_FORMAT')(mediainfo.type)
# 获取重命名后的名称
path = handler.get_rename_path(
template_string=rename_format,
@@ -631,7 +630,7 @@ class FileManagerModule(_ModuleBase):
# 媒体分类路径
dir_path = handler.get_dest_dir(mediainfo=mediainfo, target_dir=dest_dir)
# 重命名格式
rename_format = settings.RENAME_FORMAT(mediainfo.type)
rename_format = get_runtime_setting('RENAME_FORMAT')(mediainfo.type)
# 元数据补上常用属性,尽可能确保重命名后的路径不出现空白
meta = self._build_library_lookup_meta(mediainfo)
# 获取路径(重命名路径)
@@ -665,9 +664,9 @@ class FileManagerModule(_ModuleBase):
continue
if media_files:
media_extensions = (
settings.RMT_AUDIOEXT
get_runtime_setting('RMT_AUDIOEXT')
if mediainfo.type == MediaType.MUSIC
else settings.RMT_MEDIAEXT
else get_runtime_setting('RMT_MEDIAEXT')
)
for media_file in media_files:
if (
@@ -692,7 +691,7 @@ class FileManagerModule(_ModuleBase):
if kwargs.get("server"):
return None
if not settings.LOCAL_EXISTS_SEARCH:
if not get_runtime_setting('LOCAL_EXISTS_SEARCH'):
return None
logger.debug(f"正在本地媒体库中查找 {mediainfo.title_year}...")
+10 -12
View File
@@ -8,19 +8,17 @@ from typing import List, Optional, Tuple, Union
import requests
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.stop import runtime_stop_state
from app.schemas.file import StorageUsage as _SchemaStorageUsage
from app.schemas.workflow import FileItem as _SchemaFileItem
settings = RuntimeSettingsCompat()
from app.adapters.network.http import RequestUtils
from app.foundation import temporal as time_tools
from app.foundation.singleton import WeakSingleton
from app.modules.filemanager.storages import StorageBase, transfer_process
from app.runtime.log import logger
from app.runtime.settings import get_runtime_setting
from app.runtime.stop import runtime_stop_state
from app.schemas.exception import StorageQueryError
from app.schemas.file import StorageUsage as _SchemaStorageUsage
from app.schemas.types import StorageSchema
from app.schemas.workflow import FileItem as _SchemaFileItem
lock = threading.Lock()
@@ -48,7 +46,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
base_url = "https://openapi.alipan.com"
# 阿里云盘目录时间不随子文件变更而更新,默认关闭目录修改时间检查
snapshot_check_folder_modtime = settings.ALIPAN_SNAPSHOT_CHECK_FOLDER_MODTIME
snapshot_check_folder_modtime = get_runtime_setting('ALIPAN_SNAPSHOT_CHECK_FOLDER_MODTIME')
# 文件块大小,默认10MB
chunk_size = 10 * 1024 * 1024
@@ -117,7 +115,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
resp = self.session.post(
f"{self.base_url}/oauth/authorize/qrcode",
json={
"client_id": settings.ALIPAN_APP_ID,
"client_id": get_runtime_setting('ALIPAN_APP_ID'),
"scopes": [
"user:base",
"file:all:read",
@@ -181,7 +179,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
resp = self.session.post(
f"{self.base_url}/oauth/access_token",
json={
"client_id": settings.ALIPAN_APP_ID,
"client_id": get_runtime_setting('ALIPAN_APP_ID'),
"grant_type": "authorization_code",
"code": self._auth_state["authCode"],
"code_verifier": self._auth_state["code_verifier"],
@@ -205,7 +203,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
resp = self.session.post(
f"{self.base_url}/oauth/access_token",
json={
"client_id": settings.ALIPAN_APP_ID,
"client_id": get_runtime_setting('ALIPAN_APP_ID'),
"grant_type": "refresh_token",
"refresh_token": refresh_token,
},
@@ -745,7 +743,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
logger.error(f"【阿里云盘】下载链接为空: {fileitem.name}")
return None
local_path = self._build_download_path(fileitem, path or settings.TEMP_PATH)
local_path = self._build_download_path(fileitem, path or get_runtime_setting('TEMP_PATH'))
if not local_path:
return None
@@ -759,7 +757,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
try:
# 构建请求头,包含必要的认证信息
headers = {
"User-Agent": settings.NORMAL_USER_AGENT,
"User-Agent": get_runtime_setting('NORMAL_USER_AGENT'),
"Referer": "https://www.aliyundrive.com/",
"Accept": "*/*",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
+7 -9
View File
@@ -5,20 +5,18 @@ from datetime import datetime
from pathlib import Path
from typing import List, Optional
from app.runtime.cache import cached
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.stop import runtime_stop_state
from app.schemas.file import StorageUsage as _SchemaStorageUsage
from app.schemas.workflow import FileItem as _SchemaFileItem
settings = RuntimeSettingsCompat()
from app.adapters.network.http import RequestUtils
from app.foundation.singleton import WeakSingleton
from app.foundation.url import UrlUtils
from app.modules.filemanager.storages import StorageBase, transfer_process
from app.runtime.cache import cached
from app.runtime.log import logger
from app.runtime.settings import get_runtime_setting
from app.runtime.stop import runtime_stop_state
from app.schemas.exception import OperationInterrupted, StorageQueryError
from app.schemas.file import StorageUsage as _SchemaStorageUsage
from app.schemas.types import StorageSchema
from app.schemas.workflow import FileItem as _SchemaFileItem
# OpenList/AList 在 per_page<=0 时会退回后端默认 200,显式指定最大页大小避免大目录被截断。
OPENLIST_MAX_LIST_PAGE_SIZE = 500
@@ -41,7 +39,7 @@ class Alist(StorageBase, metaclass=WeakSingleton):
}
# 快照检查目录修改时间
snapshot_check_folder_modtime = settings.OPENLIST_SNAPSHOT_CHECK_FOLDER_MODTIME
snapshot_check_folder_modtime = get_runtime_setting('OPENLIST_SNAPSHOT_CHECK_FOLDER_MODTIME')
def __init__(self):
super().__init__()
@@ -692,7 +690,7 @@ class Alist(StorageBase, metaclass=WeakSingleton):
download_url = download_url + "?sign=" + result["data"]["sign"]
if not path:
local_path = settings.TEMP_PATH / fileitem.name
local_path = get_runtime_setting('TEMP_PATH') / fileitem.name
else:
local_path = path / fileitem.name
+5 -7
View File
@@ -4,19 +4,17 @@ import time
from pathlib import Path
from typing import List, Optional
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.stop import runtime_stop_state
from app.schemas.file import StorageUsage as _SchemaStorageUsage
from app.schemas.workflow import FileItem as _SchemaFileItem
settings = RuntimeSettingsCompat()
from app.adapters.system.fsproxy import fsproxy
from app.adapters.system.host import SystemUtils
from app.application.directory import DirectoryHelper
from app.modules.filemanager.storages import StorageBase, transfer_process
from app.runtime.log import logger
from app.runtime.settings import get_runtime_setting
from app.runtime.stop import runtime_stop_state
from app.schemas.exception import StorageQueryError
from app.schemas.file import StorageUsage as _SchemaStorageUsage
from app.schemas.types import StorageSchema
from app.schemas.workflow import FileItem as _SchemaFileItem
class LocalStorage(StorageBase):
@@ -477,7 +475,7 @@ class LocalStorage(StorageBase):
total_storage, free_storage = SystemUtils.space_usage(
[Path(d.download_path) for d in directory_helper.get_local_download_dirs() if d.download_path] +
[Path(d.library_path) for d in directory_helper.get_local_library_dirs() if d.library_path],
btrfs_fsid_dedup=settings.BTRFS_FSID_DEDUP,
btrfs_fsid_dedup=get_runtime_setting('BTRFS_FSID_DEDUP'),
)
return _SchemaStorageUsage(
total=total_storage,
+5 -7
View File
@@ -6,17 +6,15 @@ from collections import OrderedDict
from pathlib import Path
from typing import List, Optional, Union
from app.runtime.settings import RuntimeSettingsCompat
from app.schemas.file import StorageUsage as _SchemaStorageUsage
from app.schemas.workflow import FileItem as _SchemaFileItem
settings = RuntimeSettingsCompat()
from app.adapters.system.host import SystemUtils
from app.foundation import temporal as time_tools
from app.modules.filemanager.storages import StorageBase, transfer_process
from app.runtime.log import logger
from app.runtime.settings import get_runtime_setting
from app.schemas.exception import StorageQueryError
from app.schemas.file import StorageUsage as _SchemaStorageUsage
from app.schemas.types import StorageSchema
from app.schemas.workflow import FileItem as _SchemaFileItem
_MAX_FOLDER_LOCKS = 4096
_folder_locks: OrderedDict[str, threading.Lock] = OrderedDict()
@@ -50,7 +48,7 @@ class Rclone(StorageBase):
"copy": "复制"
}
snapshot_check_folder_modtime = settings.RCLONE_SNAPSHOT_CHECK_FOLDER_MODTIME
snapshot_check_folder_modtime = get_runtime_setting('RCLONE_SNAPSHOT_CHECK_FOLDER_MODTIME')
def init_storage(self):
"""
@@ -376,7 +374,7 @@ class Rclone(StorageBase):
"""
带实时进度显示的下载
"""
local_path = self._build_download_path(fileitem, path or settings.TEMP_PATH)
local_path = self._build_download_path(fileitem, path or get_runtime_setting('TEMP_PATH'))
if not local_path:
return None
+5 -7
View File
@@ -12,17 +12,15 @@ from smbprotocol.exceptions import (
SMBResponseException,
)
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.stop import runtime_stop_state
from app.schemas.file import StorageUsage as _SchemaStorageUsage
from app.schemas.workflow import FileItem as _SchemaFileItem
settings = RuntimeSettingsCompat()
from app.foundation.singleton import WeakSingleton
from app.modules.filemanager.storages import StorageBase, transfer_process
from app.runtime.log import logger
from app.runtime.settings import get_runtime_setting
from app.runtime.stop import runtime_stop_state
from app.schemas.exception import StorageQueryError
from app.schemas.file import StorageUsage as _SchemaStorageUsage
from app.schemas.types import StorageSchema
from app.schemas.workflow import FileItem as _SchemaFileItem
lock = threading.Lock()
@@ -550,7 +548,7 @@ class SMB(StorageBase, metaclass=WeakSingleton):
"""
带实时进度显示的下载
"""
local_path = self._build_download_path(fileitem, path or settings.TEMP_PATH)
local_path = self._build_download_path(fileitem, path or get_runtime_setting('TEMP_PATH'))
if not local_path:
return None
smb_path = self._normalize_path(fileitem.path)
+8 -10
View File
@@ -12,19 +12,17 @@ from cryptography.hazmat.primitives import hashes
from oss2 import SizedFileAdapter, determine_part_size
from oss2.models import PartInfo
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.stop import runtime_stop_state
from app.schemas.file import StorageUsage as _SchemaStorageUsage
from app.schemas.workflow import FileItem as _SchemaFileItem
settings = RuntimeSettingsCompat()
from app.foundation import size as size_tools
from app.foundation.singleton import WeakSingleton
from app.modules.filemanager.storages import StorageBase, transfer_process
from app.runtime.log import logger
from app.runtime.rate import QpsRateLimiter, RateStats
from app.runtime.settings import get_runtime_setting
from app.runtime.stop import runtime_stop_state
from app.schemas.exception import StorageQueryError
from app.schemas.file import StorageUsage as _SchemaStorageUsage
from app.schemas.types import StorageSchema
from app.schemas.workflow import FileItem as _SchemaFileItem
lock = Lock()
@@ -130,7 +128,7 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
生成 OAuth2 授权 URL
"""
try:
resp = self.session.get(f"{settings.U115_AUTH_SERVER}/u115/auth_url")
resp = self.session.get(f"{get_runtime_setting('U115_AUTH_SERVER')}/u115/auth_url")
if resp is None:
return {}, "无法连接到授权服务器"
@@ -165,7 +163,7 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
resp = self.session.post(
"https://passportapi.115.com/open/authDeviceCode",
data={
"client_id": settings.U115_APP_ID,
"client_id": get_runtime_setting('U115_APP_ID'),
"code_challenge": code_challenge,
"code_challenge_method": "sha256",
},
@@ -229,7 +227,7 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
try:
resp = self.session.get(
f"{settings.U115_AUTH_SERVER}/u115/token", params={"state": state}
f"{get_runtime_setting('U115_AUTH_SERVER')}/u115/token", params={"state": state}
)
if resp is None:
return {}, "无法连接到授权服务器"
@@ -910,7 +908,7 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
logger.error(f"【115】下载链接为空: {fileitem.name}")
return None
local_path = self._build_download_path(fileitem, path or settings.TEMP_PATH)
local_path = self._build_download_path(fileitem, path or get_runtime_setting('TEMP_PATH'))
if not local_path:
return None
+10 -11
View File
@@ -4,9 +4,8 @@ from typing import Optional, List, Tuple
from jinja2 import Template
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.domain.context import MediaInfo, MusicInfo
from app.runtime.events import eventmanager
from app.domain.meta.metabase import MetaBase
@@ -204,7 +203,7 @@ class TransHandler:
"""
if not _fileitem.extension:
return False
if f".{_fileitem.extension.lower()}" in settings.RMT_SUBEXT:
if f".{_fileitem.extension.lower()}" in get_runtime_setting('RMT_SUBEXT'):
return True
return False
@@ -224,11 +223,11 @@ class TransHandler:
if not _fileitem.extension:
return False
extension = f".{_fileitem.extension.lower()}"
if extension in settings.RMT_SUBEXT:
if extension in get_runtime_setting('RMT_SUBEXT'):
return True
if __is_music_lyrics_file(_fileitem):
return True
if mediainfo.type != MediaType.MUSIC and extension in settings.RMT_AUDIOEXT:
if mediainfo.type != MediaType.MUSIC and extension in get_runtime_setting('RMT_AUDIOEXT'):
return True
return False
@@ -252,7 +251,7 @@ class TransHandler:
try:
# 重命名格式
rename_format = settings.RENAME_FORMAT(mediainfo.type)
rename_format = get_runtime_setting('RENAME_FORMAT')(mediainfo.type)
# 判断是否为文件夹
if fileitem.type == "dir":
@@ -953,10 +952,10 @@ class TransHandler:
# 添加默认字幕标识
if (
(settings.DEFAULT_SUB == "zh-cn" and new_file_type == ".chi.zh-cn")
or (settings.DEFAULT_SUB == "zh-tw" and new_file_type == ".zh-tw")
or (settings.DEFAULT_SUB == "ja" and new_file_type == ".ja")
or (settings.DEFAULT_SUB == "eng" and new_file_type == ".eng")
(get_runtime_setting('DEFAULT_SUB') == "zh-cn" and new_file_type == ".chi.zh-cn")
or (get_runtime_setting('DEFAULT_SUB') == "zh-tw" and new_file_type == ".zh-tw")
or (get_runtime_setting('DEFAULT_SUB') == "ja" and new_file_type == ".ja")
or (get_runtime_setting('DEFAULT_SUB') == "eng" and new_file_type == ".eng")
):
new_sub_tag = ".default" + new_file_type
else:
@@ -1283,7 +1282,7 @@ class TransHandler:
if media_file.type != "file":
continue
# 当前只有视频文件需要保留最新版本,其余格式无需处理,以避免误删 (issue 5449)
if f".{media_file.extension.lower()}" not in settings.RMT_MEDIAEXT:
if f".{media_file.extension.lower()}" not in get_runtime_setting('RMT_MEDIAEXT'):
continue
# 识别文件中的季集信息
filemeta = MetaInfoPath(media_path)
+6 -8
View File
@@ -12,10 +12,8 @@ from app.domain.scraper import MediaScraperHelper
from app.foundation.text import convert as zhconv_convert
from app.modules import _ModuleBase
from app.modules._base.media_auxiliary import MediaAuxiliaryProviderMixin
from app.runtime.settings import RuntimeSettingsCompat
settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.runtime.settings import get_runtime_setting
from app.schemas.context import MediaCredit, MediaImageSet
from app.schemas.media import normalize_media_source
from app.schemas.types import (
@@ -59,7 +57,7 @@ class ImdbModule(MediaAuxiliaryProviderMixin, _ModuleBase):
def init_module(self) -> None:
"""按当前代理配置初始化 IMDb 客户端和通用刮削器。"""
self._config = ImdbConfigSnapshot(proxy=settings.PROXY)
self._config = ImdbConfigSnapshot(proxy=get_runtime_setting('PROXY'))
self.imdb_api = ImdbApi(proxies=self._config.proxy)
self.scraper = MediaScraperHelper()
@@ -517,7 +515,7 @@ class ImdbModule(MediaAuxiliaryProviderMixin, _ModuleBase):
if requested_source not in {None, MediaSource.IMDb}:
return None
selected_source = requested_source or normalize_media_source(
settings.RECOGNIZE_SOURCE
get_runtime_setting('RECOGNIZE_SOURCE')
)
if selected_source != MediaSource.IMDb or not meta or not meta.name:
return None
@@ -560,7 +558,7 @@ class ImdbModule(MediaAuxiliaryProviderMixin, _ModuleBase):
if requested_source not in {None, MediaSource.IMDb}:
return None
selected_source = requested_source or normalize_media_source(
settings.RECOGNIZE_SOURCE
get_runtime_setting('RECOGNIZE_SOURCE')
)
if selected_source != MediaSource.IMDb or not meta or not meta.name:
return None
@@ -657,7 +655,7 @@ class ImdbModule(MediaAuxiliaryProviderMixin, _ModuleBase):
) -> Optional[str]:
"""生成 IMDb 来源的 NFO 元数据文本。"""
del kwargs
if (mediainfo.scrape_source or settings.SCRAP_SOURCE) != MediaSource.IMDb.value:
if (mediainfo.scrape_source or get_runtime_setting('SCRAP_SOURCE')) != MediaSource.IMDb.value:
return None
if not self.scraper:
return None
@@ -672,7 +670,7 @@ class ImdbModule(MediaAuxiliaryProviderMixin, _ModuleBase):
episode: Optional[int] = None,
) -> Optional[dict]:
"""生成 IMDb 来源的图片文件名与下载地址映射。"""
if (mediainfo.scrape_source or settings.SCRAP_SOURCE) != MediaSource.IMDb.value:
if (mediainfo.scrape_source or get_runtime_setting('SCRAP_SOURCE')) != MediaSource.IMDb.value:
return None
if not self.scraper:
return None
+11 -13
View File
@@ -9,11 +9,9 @@ from pydantic import BaseModel, ConfigDict, Field, ValidationError
from app.adapters.network.http import AsyncRequestUtils, RequestUtils
from app.runtime.cache import cached
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.tasks import get_task_registry
settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.runtime.settings import get_runtime_setting
from app.runtime.tasks import get_task_registry
TModel = TypeVar("TModel", bound=BaseModel)
@@ -227,7 +225,7 @@ class ImdbApi:
def __init__(self, proxies: Optional[dict] = None) -> None:
"""按一次模块配置快照创建网络请求适配器。"""
headers = {
"User-Agent": settings.NORMAL_USER_AGENT,
"User-Agent": get_runtime_setting('NORMAL_USER_AGENT'),
"Accept": "application/graphql+json, application/json",
"Content-Type": "application/json",
"x-imdb-client-name": "imdb-web-next-localized",
@@ -262,8 +260,8 @@ class ImdbApi:
return cls._freeze_value(params or {})
@cached(
maxsize=settings.CONF.imdb,
ttl=settings.CONF.meta,
maxsize=get_runtime_setting('CONF').imdb,
ttl=get_runtime_setting('CONF').meta,
skip_none=True,
shared_key="imdb_get",
)
@@ -274,8 +272,8 @@ class ImdbApi:
return self._request.get_json(url, params=dict(params_key))
@cached(
maxsize=settings.CONF.imdb,
ttl=settings.CONF.meta,
maxsize=get_runtime_setting('CONF').imdb,
ttl=get_runtime_setting('CONF').meta,
skip_none=True,
shared_key="imdb_get",
)
@@ -286,8 +284,8 @@ class ImdbApi:
return await self._async_request.get_json(url, params=dict(params_key))
@cached(
maxsize=settings.CONF.imdb,
ttl=settings.CONF.meta,
maxsize=get_runtime_setting('CONF').imdb,
ttl=get_runtime_setting('CONF').meta,
skip_none=True,
shared_key="imdb_graphql",
skip_if=_is_graphql_error,
@@ -302,8 +300,8 @@ class ImdbApi:
)
@cached(
maxsize=settings.CONF.imdb,
ttl=settings.CONF.meta,
maxsize=get_runtime_setting('CONF').imdb,
ttl=get_runtime_setting('CONF').meta,
skip_none=True,
shared_key="imdb_graphql",
skip_if=_is_graphql_error,
+5 -6
View File
@@ -8,9 +8,8 @@ from urllib.parse import urljoin, urlsplit
from requests import Session
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.adapters.network.cloudflare import under_challenge
from app.runtime.log import logger
from app.adapters.network.http import RequestUtils
@@ -228,7 +227,7 @@ class SiteParserBase(metaclass=ABCMeta):
)
)
# 解析用户未读消息
if settings.SITE_MESSAGE:
if get_runtime_setting('SITE_MESSAGE'):
self._pase_unread_msgs()
# 解析用户上传、下载、分享率等信息
if self._user_traffic_page:
@@ -346,7 +345,7 @@ class SiteParserBase(metaclass=ABCMeta):
:return:
"""
req_headers = None
proxies = settings.PROXY if self._proxy else None
proxies = get_runtime_setting('PROXY') if self._proxy else None
if self._ua or headers or self._addition_headers:
if self.request_mode == "apikey":
@@ -408,8 +407,8 @@ class SiteParserBase(metaclass=ABCMeta):
f"{self._site_name} 检测到Cloudflare,请更新Cookie和UA")
return ""
return RequestUtils.get_decoded_html_content(res,
settings.ENCODING_DETECTION_PERFORMANCE_MODE,
settings.ENCODING_DETECTION_MIN_CONFIDENCE)
get_runtime_setting('ENCODING_DETECTION_PERFORMANCE_MODE'),
get_runtime_setting('ENCODING_DETECTION_MIN_CONFIDENCE'))
return ""
+3 -4
View File
@@ -4,9 +4,8 @@ from urllib.parse import urljoin
from typing import Optional, Tuple
from app.runtime.log import logger
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.adapters.network.http import RequestUtils
from app.domain import site as site_rules
from app.foundation import temporal as time_tools
@@ -195,7 +194,7 @@ class RousiSiteUserInfo(SiteParserBase):
res = RequestUtils(
headers=headers,
timeout=60,
proxies=settings.PROXY if self._proxy else None
proxies=get_runtime_setting('PROXY') if self._proxy else None
).get_res(
url=urljoin(self._base_url, "api/messages"),
params=params
@@ -231,7 +230,7 @@ class RousiSiteUserInfo(SiteParserBase):
RequestUtils(
headers=headers,
timeout=60,
proxies=settings.PROXY if self._proxy else None
proxies=get_runtime_setting('PROXY') if self._proxy else None
).post_res(
url=urljoin(self._base_url, "api/messages/read-all")
)
+8 -9
View File
@@ -9,9 +9,8 @@ from jinja2 import Template
from pyquery import PyQuery
from app.runtime.execution import run_in_threadpool
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.schemas.types import MediaType
from app.adapters.system import rust as rust_accel
@@ -136,9 +135,9 @@ class SiteSpider:
self.page = page
if self.domain and not str(self.domain).endswith("/"):
self.domain = self.domain + "/"
self.ua = indexer.get('ua') or settings.USER_AGENT
self.proxies = settings.PROXY if indexer.get('proxy') else None
self.proxy_server = settings.PROXY_SERVER if indexer.get('proxy') else None
self.ua = indexer.get('ua') or get_runtime_setting('USER_AGENT')
self.proxies = get_runtime_setting('PROXY') if indexer.get('proxy') else None
self.proxy_server = get_runtime_setting('PROXY_SERVER') if indexer.get('proxy') else None
self.cookie = indexer.get('cookie')
self.referer = referer
# 初始化属性
@@ -362,8 +361,8 @@ class SiteSpider:
return self.parse(
RequestUtils.get_decoded_html_content(
ret,
performance_mode=settings.ENCODING_DETECTION_PERFORMANCE_MODE,
confidence_threshold=settings.ENCODING_DETECTION_MIN_CONFIDENCE
performance_mode=get_runtime_setting('ENCODING_DETECTION_PERFORMANCE_MODE'),
confidence_threshold=get_runtime_setting('ENCODING_DETECTION_MIN_CONFIDENCE')
)
)
@@ -394,8 +393,8 @@ class SiteSpider:
self.parse,
RequestUtils.get_decoded_html_content(
ret,
performance_mode=settings.ENCODING_DETECTION_PERFORMANCE_MODE,
confidence_threshold=settings.ENCODING_DETECTION_MIN_CONFIDENCE
performance_mode=get_runtime_setting('ENCODING_DETECTION_PERFORMANCE_MODE'),
confidence_threshold=get_runtime_setting('ENCODING_DETECTION_MIN_CONFIDENCE')
)
)
+2 -3
View File
@@ -1,9 +1,8 @@
import urllib.parse
from typing import Tuple, List
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.application.configuration import get_configured_system_config
from app.runtime.log import logger
from app.schemas.types import MediaType
@@ -70,7 +69,7 @@ class HaiDanSpider:
self._searchurl = self._searchurl % self._url
self._name = indexer.get('name')
if indexer.get('proxy'):
self._proxy = settings.PROXY
self._proxy = get_runtime_setting('PROXY')
self._cookie = indexer.get('cookie')
self._ua = indexer.get('ua')
self._timeout = indexer.get('timeout') or 15
+2 -3
View File
@@ -1,8 +1,7 @@
from typing import Tuple, List, Optional
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.application.configuration import get_configured_system_config
from app.runtime.log import logger
from app.schemas.types import MediaType
@@ -76,7 +75,7 @@ class HddolbySpider:
self._domain_host = site_rules.extract_domain(self._domain)
self._name = indexer.get('name')
if indexer.get('proxy'):
self._proxy = settings.PROXY
self._proxy = get_runtime_setting('PROXY')
self._cookie = indexer.get('cookie')
self._ua = indexer.get('ua')
self._apikey = indexer.get('apikey')
+2 -3
View File
@@ -4,9 +4,8 @@ import re
from typing import Tuple, List, Optional
from urllib.parse import urlparse
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.application.configuration import get_configured_system_config
from app.runtime.log import logger
from app.schemas.types import MediaType
@@ -75,7 +74,7 @@ class MTorrentSpider:
self._searchurl = self._searchurl % self._domain
self._name = indexer.get('name')
if indexer.get('proxy'):
self._proxy = settings.PROXY
self._proxy = get_runtime_setting('PROXY')
self._cookie = indexer.get('cookie')
self._ua = indexer.get('ua')
self._apikey = indexer.get('apikey')
+2 -3
View File
@@ -2,9 +2,8 @@ import base64
import json
from typing import List, Optional, Tuple
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.application.configuration import get_configured_system_config
from app.runtime.log import logger
from app.schemas.types import MediaType
@@ -60,7 +59,7 @@ class RousiSpider:
self._downloadurl = self._downloadurl % (self._domain, "%s")
self._name = indexer.get('name')
if indexer.get('proxy'):
self._proxy = settings.PROXY
self._proxy = get_runtime_setting('PROXY')
self._cookie = indexer.get('cookie')
self._ua = indexer.get('ua')
self._apikey = indexer.get('apikey')
+3 -4
View File
@@ -3,9 +3,8 @@ import json
import time
from typing import List, Optional, Tuple
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.schemas.types import MediaType
from app.adapters.network.http import AsyncRequestUtils, RequestUtils
@@ -34,9 +33,9 @@ class SunnyPTSpider:
self._api_url = str(
indexer.get("api_url") or "https://api.sunnypt.top/api/v1/mp"
).rstrip("/")
self._proxy = settings.PROXY if indexer.get("proxy") else None
self._proxy = get_runtime_setting('PROXY') if indexer.get("proxy") else None
self._use_proxy = bool(indexer.get("proxy"))
self._user_agent = indexer.get("ua") or settings.USER_AGENT
self._user_agent = indexer.get("ua") or get_runtime_setting('USER_AGENT')
self._api_key = indexer.get("apikey")
self._timeout = indexer.get("timeout") or 15
self._configured_categories = self._parse_configured_categories(
+2 -3
View File
@@ -2,9 +2,8 @@ import re
from typing import Tuple, List, Optional
from app.runtime.cache import cached
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.adapters.network.http import RequestUtils, AsyncRequestUtils
from app.foundation.singleton import SingletonClass
@@ -33,7 +32,7 @@ class TNodeSpider(metaclass=SingletonClass):
self._searchurl = self._baseurl % self._domain
self._name = indexer.get('name')
if indexer.get('proxy'):
self._proxy = settings.PROXY
self._proxy = get_runtime_setting('PROXY')
self._cookie = indexer.get('cookie')
self._ua = indexer.get('ua')
self._timeout = indexer.get('timeout') or 15
+2 -3
View File
@@ -1,9 +1,8 @@
from typing import List, Tuple, Optional
from urllib.parse import quote
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.schemas.types import MediaType
from app.adapters.network.http import RequestUtils, AsyncRequestUtils
@@ -34,7 +33,7 @@ class TorrentLeech:
"""初始化站点认证信息和媒体分类配置。"""
self._indexer = indexer
if indexer.get('proxy'):
self._proxy = settings.PROXY
self._proxy = get_runtime_setting('PROXY')
self._timeout = indexer.get('timeout') or 15
def __category_ids(self, mtype: MediaType = None) -> List[str]:
+3 -4
View File
@@ -2,9 +2,8 @@ import base64
import json
from typing import List, Optional, Tuple
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.schemas.types import MediaType
from app.adapters.network.http import AsyncRequestUtils, RequestUtils
@@ -46,9 +45,9 @@ class YemaSpider:
indexer = indexer or {}
self._name = indexer.get("name") or "YemaPT"
self._site_url = str(indexer.get("domain") or "https://www.yemapt.org/").rstrip("/")
self._proxy = settings.PROXY if indexer.get("proxy") else None
self._proxy = get_runtime_setting('PROXY') if indexer.get("proxy") else None
self._use_proxy = bool(indexer.get("proxy"))
self._user_agent = indexer.get("ua") or settings.USER_AGENT
self._user_agent = indexer.get("ua") or get_runtime_setting('USER_AGENT')
self._api_key = indexer.get("apikey")
self._timeout = indexer.get("timeout") or 15
self._search_url = f"{self._site_url}/openApi/torrent/fetchOpenTorrentList.json"
+4 -5
View File
@@ -9,9 +9,8 @@ from app.schemas.mediaserver import MediaServerItem as _SchemaMediaServerItem
from app.schemas.mediaserver import MediaServerLibrary as _SchemaMediaServerLibrary
from app.schemas.mediaserver import MediaServerPlayItem as _SchemaMediaServerPlayItem
from app.schemas.mediaserver import WebhookEventInfo as _SchemaWebhookEventInfo
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.application.mediaserver import MediaServerIdentityHelper, format_emby_family_item
from app.runtime.log import logger
from app.schemas.types import MediaType
@@ -40,7 +39,7 @@ class Jellyfin:
if self._playhost:
self._playhost = UrlUtils.standardize_base_url(self._playhost)
self._apikey = apikey
self.user = self.get_user(settings.SUPERUSER)
self.user = self.get_user(get_runtime_setting('SUPERUSER'))
self.serverid = self.get_server_id()
self._sync_libraries = sync_libraries or []
@@ -253,9 +252,9 @@ class Jellyfin:
for user in users:
if user.get("Name") == user_name:
return user.get("Id")
if user_name == settings.SUPERUSER:
if user_name == get_runtime_setting('SUPERUSER'):
logger.warning(
"MoviePilot 当前配置的超级管理员用户名为 {},请确保Jellyfin中存在同名管理员账号,否则可能无法正常使用部分功能!".format(settings.SUPERUSER)
"MoviePilot 当前配置的超级管理员用户名为 {},请确保Jellyfin中存在同名管理员账号,否则可能无法正常使用部分功能!".format(get_runtime_setting('SUPERUSER'))
)
# 查询管理员,优先选择同时具备全库访问能力的账号,再回退到普通管理员。
# 获取总媒体库数量
+6 -7
View File
@@ -1,9 +1,8 @@
from typing import Any, Optional, Tuple, Union
from app.runtime.cache import cached
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.domain.context import MusicInfo
from app.runtime.log import logger
from app.modules import _ModuleBase
@@ -155,7 +154,7 @@ class ListenBrainzModule(_ModuleBase):
)
@classmethod
@cached(maxsize=settings.CONF.listenbrainz, ttl=settings.CONF.meta, skip_none=True)
@cached(maxsize=get_runtime_setting('CONF').listenbrainz, ttl=get_runtime_setting('CONF').meta, skip_none=True)
def _request_json(
cls,
path: str,
@@ -164,10 +163,10 @@ class ListenBrainzModule(_ModuleBase):
"""请求 ListenBrainz JSON 接口并统一处理网络和响应错误。"""
response = RequestUtils(
headers={
"User-Agent": f"{settings.USER_AGENT} (https://github.com/jxxghp/MoviePilot)",
"User-Agent": f"{get_runtime_setting('USER_AGENT')} (https://github.com/jxxghp/MoviePilot)",
"Accept": "application/json",
},
proxies=settings.PROXY,
proxies=get_runtime_setting('PROXY'),
timeout=20,
).get_res(f"{cls._base_url}{path}", params=params)
if response is None:
@@ -281,7 +280,7 @@ class ListenBrainzModule(_ModuleBase):
if not release_mbid:
return None
# 支持配置音乐封面代理地址,解决 coverartarchive.org 无法访问的问题
base = (settings.MUSIC_COVER_PROXY or "https://coverartarchive.org").rstrip("/")
base = (get_runtime_setting('MUSIC_COVER_PROXY') or "https://coverartarchive.org").rstrip("/")
return f"{base}/release/{release_mbid}/front-500"
@classmethod
@@ -290,7 +289,7 @@ class ListenBrainzModule(_ModuleBase):
if not release_group_id:
return None
# 支持配置音乐封面代理地址,解决 coverartarchive.org 无法访问的问题
base = (settings.MUSIC_COVER_PROXY or "https://coverartarchive.org").rstrip("/")
base = (get_runtime_setting('MUSIC_COVER_PROXY') or "https://coverartarchive.org").rstrip("/")
return f"{base}/release-group/{release_group_id}/front-500"
@staticmethod
+5 -6
View File
@@ -9,10 +9,9 @@ from app.domain.meta.metamusic import MetaMusic
from app.modules import _ModuleBase
from app.runtime.cache import cached
from app.runtime.log import logger
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
from app.schemas.types import ModuleType, OtherModulesType
settings = RuntimeSettingsCompat()
class LrclibModule(_ModuleBase):
@@ -201,13 +200,13 @@ class LrclibModule(_ModuleBase):
time.sleep(delay)
response = RequestUtils(
headers={
"User-Agent": f"{settings.USER_AGENT} (https://github.com/jxxghp/MoviePilot)",
"User-Agent": f"{get_runtime_setting('USER_AGENT')} (https://github.com/jxxghp/MoviePilot)",
"Accept": "application/json",
},
proxies=settings.PROXY,
proxies=get_runtime_setting('PROXY'),
timeout=20,
).get_res(
f"{(base_url or str(settings.LRCLIB_BASE_URL)).rstrip('/')}{path}",
f"{(base_url or str(get_runtime_setting('LRCLIB_BASE_URL'))).rstrip('/')}{path}",
params=params,
)
cls._last_request_at = time.monotonic()
@@ -231,7 +230,7 @@ class LrclibModule(_ModuleBase):
if response.status_code in (429, 503):
retry_after = cls._retry_after_seconds(response.headers.get("Retry-After"))
response.close()
max_wait = max(int(settings.LYRICS_PROVIDER_RETRY_MAX_WAIT), 0)
max_wait = max(int(get_runtime_setting('LYRICS_PROVIDER_RETRY_MAX_WAIT')), 0)
if retry_after > max_wait:
cls._cooldown_until = time.monotonic() + retry_after
logger.warning(f"LRCLIB 进入冷却 {retry_after:g} 秒,跳过当前批次后续请求")
+9 -10
View File
@@ -8,9 +8,8 @@ from typing import Any, Iterable, Optional, Tuple, Union
from requests import Session
from app.runtime.cache import cached
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.domain.context import (
MusicAlbumInfo,
MusicArtistInfo,
@@ -1761,7 +1760,7 @@ class MusicBrainzModule(_ModuleBase):
if not release_group_id:
return None
# 支持配置音乐封面代理地址,解决 coverartarchive.org 无法访问的问题
base = (settings.MUSIC_COVER_PROXY or "https://coverartarchive.org").rstrip("/")
base = (get_runtime_setting('MUSIC_COVER_PROXY') or "https://coverartarchive.org").rstrip("/")
return f"{base}/release-group/{release_group_id}/front-500"
@classmethod
@@ -1795,7 +1794,7 @@ class MusicBrainzModule(_ModuleBase):
await asyncio.sleep(delay)
@classmethod
@cached(maxsize=settings.CONF.musicbrainz, ttl=settings.CONF.meta, skip_none=True)
@cached(maxsize=get_runtime_setting('CONF').musicbrainz, ttl=get_runtime_setting('CONF').meta, skip_none=True)
def _request_json(
cls,
path: str,
@@ -1811,10 +1810,10 @@ class MusicBrainzModule(_ModuleBase):
cls._wait_for_rate_limit()
response = RequestUtils(
headers={
"User-Agent": f"{settings.USER_AGENT} (https://github.com/jxxghp/MoviePilot)",
"User-Agent": f"{get_runtime_setting('USER_AGENT')} (https://github.com/jxxghp/MoviePilot)",
"Accept": "application/json",
},
proxies=settings.PROXY,
proxies=get_runtime_setting('PROXY'),
session=cls._get_session(),
timeout=20,
).get_res(f"{cls._base_url}{path}", params=params)
@@ -1850,8 +1849,8 @@ class MusicBrainzModule(_ModuleBase):
@classmethod
@cached(
maxsize=settings.CONF.musicbrainz,
ttl=settings.CONF.meta,
maxsize=get_runtime_setting('CONF').musicbrainz,
ttl=get_runtime_setting('CONF').meta,
skip_none=True,
shared_key="_request_json",
)
@@ -1866,10 +1865,10 @@ class MusicBrainzModule(_ModuleBase):
await cls._async_wait_for_rate_limit()
response = await AsyncRequestUtils(
headers={
"User-Agent": f"{settings.USER_AGENT} (https://github.com/jxxghp/MoviePilot)",
"User-Agent": f"{get_runtime_setting('USER_AGENT')} (https://github.com/jxxghp/MoviePilot)",
"Accept": "application/json",
},
proxies=settings.PROXY,
proxies=get_runtime_setting('PROXY'),
timeout=20,
).get_res(f"{cls._base_url}{path}", params=params)
if response is None:
+4 -5
View File
@@ -6,9 +6,8 @@ from time import time
from typing import Optional
from app.runtime.cache import FileCache, TTLCache
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.domain.context import MusicInfo
from app.domain.meta.metamusic import MetaMusic
from app.runtime.log import logger
@@ -37,15 +36,15 @@ class MusicBrainzCache(metaclass=WeakSingleton):
def __init__(self):
"""初始化音乐识别缓存并恢复未过期的持久化数据。"""
self.maxsize = settings.CONF.musicbrainz
self.ttl = settings.CONF.meta
self.maxsize = get_runtime_setting('CONF').musicbrainz
self.ttl = get_runtime_setting('CONF').meta
self.region = "__musicbrainz_cache__"
self._cache = TTLCache(region=self.region, maxsize=self.maxsize, ttl=self.ttl)
self._expires_at: dict[str, float] = {}
self._dirty = False
self._file_cache = None
if not self._cache.is_redis():
self._file_cache = FileCache(base=settings.CACHE_PATH, ttl=self.ttl)
self._file_cache = FileCache(base=get_runtime_setting('CACHE_PATH'), ttl=self.ttl)
self._restore()
def _restore(self) -> None:
+6 -8
View File
@@ -6,11 +6,9 @@ from app.domain.context import MusicInfo, MusicLyrics
from app.domain.meta.metamusic import MetaMusic
from app.modules import _ModuleBase
from app.runtime.log import logger
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
from app.schemas.types import ModuleType, OtherModulesType
settings = RuntimeSettingsCompat()
class MusixmatchModule(_ModuleBase):
"""使用用户授权的 Musixmatch 官方 API 获取同步或纯文本歌词。"""
@@ -30,7 +28,7 @@ class MusixmatchModule(_ModuleBase):
def test(self) -> Tuple[bool, str]:
"""验证 API Key 和官方接口连通性。"""
if not str(settings.MUSIXMATCH_API_KEY or "").strip():
if not str(get_runtime_setting('MUSIXMATCH_API_KEY') or "").strip():
return False, "Musixmatch API Key 未配置"
payload = self._request("matcher.lyrics.get", {"q_track": "test", "q_artist": "test"})
return (True, "") if payload is not None else (False, "Musixmatch API 连接或授权失败")
@@ -106,15 +104,15 @@ class MusixmatchModule(_ModuleBase):
def _request(self, method: str, params: dict[str, Any]) -> Optional[dict[str, Any]]:
"""请求官方 API,并对限流或服务过载设置进程内冷却。"""
api_key = str(settings.MUSIXMATCH_API_KEY or "").strip()
api_key = str(get_runtime_setting('MUSIXMATCH_API_KEY') or "").strip()
if not api_key or time.monotonic() < self._cooldown_until:
return None
response = RequestUtils(
ua=settings.USER_AGENT,
proxies=settings.PROXY,
ua=get_runtime_setting('USER_AGENT'),
proxies=get_runtime_setting('PROXY'),
timeout=20,
).get_res(
f"{str(settings.MUSIXMATCH_BASE_URL).rstrip('/')}/{method}",
f"{str(get_runtime_setting('MUSIXMATCH_BASE_URL')).rstrip('/')}/{method}",
params={**params, "apikey": api_key},
)
if response is None:
+2 -4
View File
@@ -2,11 +2,9 @@ from typing import Tuple, Union
from app.application.database import get_database_governance
from app.modules import _ModuleBase
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
from app.schemas.types import ModuleType, OtherModulesType
settings = RuntimeSettingsCompat()
class PostgreSQLModule(_ModuleBase):
"""
@@ -55,7 +53,7 @@ class PostgreSQLModule(_ModuleBase):
"""
测试模块连接性
"""
if settings.DB_TYPE != "postgresql":
if get_runtime_setting('DB_TYPE') != "postgresql":
return None
error = get_database_governance().test()
if error:
+7 -8
View File
@@ -4,7 +4,7 @@ from typing import Set, Tuple, Optional, Union, List, Dict
from app.schemas.dashboard import DownloaderInfo as _SchemaDownloaderInfo
from app.domain.metainfo import MetaInfo
from app.runtime.log import logger
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
from app.modules._base import _DownloaderModuleBase
from app.modules.qbittorrent.qbittorrent import Qbittorrent
from app.schemas.transfer import DownloaderFile, DownloaderTorrent
@@ -19,7 +19,6 @@ from app.foundation import size as size_tools
from app.foundation import temporal as time_tools
from app.foundation import text as text_tools
settings = RuntimeSettingsCompat()
_QBITTORRENT_DOWNLOADING_STATES = {
"allocating",
@@ -128,8 +127,8 @@ class QbittorrentModule(_DownloaderModuleBase[Qbittorrent]):
tag = text_tools.random_string(10)
if label:
tags = label.split(',') + [tag]
elif settings.TORRENT_TAG:
tags = [tag, settings.TORRENT_TAG]
elif get_runtime_setting('TORRENT_TAG'):
tags = [tag, get_runtime_setting('TORRENT_TAG')]
else:
tags = [tag]
# 如果要选择文件则先暂停
@@ -163,9 +162,9 @@ class QbittorrentModule(_DownloaderModuleBase[Qbittorrent]):
# 给种子打上标签
if "已整理" in torrent_tags:
server.remove_torrents_tag(ids=torrent_hash, tag=['已整理'])
if settings.TORRENT_TAG and settings.TORRENT_TAG not in torrent_tags:
logger.info(f"给种子 {torrent_hash} 打上标签:{settings.TORRENT_TAG}")
server.set_torrents_tag(ids=torrent_hash, tags=[settings.TORRENT_TAG])
if get_runtime_setting('TORRENT_TAG') and get_runtime_setting('TORRENT_TAG') not in torrent_tags:
logger.info(f"给种子 {torrent_hash} 打上标签:{get_runtime_setting('TORRENT_TAG')}")
server.set_torrents_tag(ids=torrent_hash, tags=[get_runtime_setting('TORRENT_TAG')])
# 获取种子内容布局: `Original: 原始, Subfolder: 创建子文件夹, NoSubfolder: 不创建子文件夹`
torrent_layout = server.get_content_layout()
return downloader or self.get_default_config_name(), torrent_hash, torrent_layout, f"下载任务已存在"
@@ -250,7 +249,7 @@ class QbittorrentModule(_DownloaderModuleBase[Qbittorrent]):
servers: Dict[str, Qbittorrent] = self.get_instances()
ret_torrents = []
query_status = self._normalize_query_status(status)
query_tags = None if include_all_tags else settings.TORRENT_TAG
query_tags = None if include_all_tags else get_runtime_setting('TORRENT_TAG')
def __get_torrent_path(torrent_data: dict) -> Path:
"""
+7 -10
View File
@@ -7,19 +7,15 @@ import hashlib
import io
import pickle
import threading
from typing import Optional, List, Tuple
from typing import List, Optional, Tuple
from PIL import Image
from app.runtime.cache import FileCache
from app.runtime.settings import RuntimeSettingsCompat
settings = RuntimeSettingsCompat()
from app.adapters.network.http import RequestUtils
from app.application.messaging.ingress import submit_message_to_host
from app.domain.context import MediaInfo, Context
from app.domain.context import Context, MediaInfo
from app.domain.metainfo import MetaInfo
from app.runtime.log import logger
from app.runtime.thread import ThreadHelper
from app.foundation import size as size_tools
from app.modules.qqbot.api import (
get_access_token,
get_gateway_url,
@@ -27,8 +23,9 @@ from app.modules.qqbot.api import (
send_proactive_group_message,
)
from app.modules.qqbot.gateway import run_gateway
from app.adapters.network.http import RequestUtils
from app.foundation import size as size_tools
from app.runtime.cache import FileCache
from app.runtime.log import logger
from app.runtime.thread import ThreadHelper
# QQ Markdown 图片展示尺寸限制,避免竖版海报被客户端拉伸变形
_DEFAULT_IMAGE_SIZE: Tuple[int, int] = (208, 320)
+2 -4
View File
@@ -2,11 +2,9 @@ from typing import Tuple, Union
from app.adapters.cache.redis import RedisHelper
from app.modules import _ModuleBase
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
from app.schemas.types import ModuleType, OtherModulesType
settings = RuntimeSettingsCompat()
class RedisModule(_ModuleBase):
"""
@@ -55,7 +53,7 @@ class RedisModule(_ModuleBase):
"""
测试模块连接性
"""
if settings.CACHE_BACKEND_TYPE != "redis":
if get_runtime_setting('CACHE_BACKEND_TYPE') != "redis":
return None
if RedisHelper().test():
return True, ""
+8 -9
View File
@@ -4,7 +4,7 @@ from typing import Set, Tuple, Optional, Union, List, Dict
from app.schemas.dashboard import DownloaderInfo as _SchemaDownloaderInfo
from app.domain.metainfo import MetaInfo
from app.runtime.log import logger
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
from app.modules._base import _DownloaderModuleBase
from app.modules.rtorrent.rtorrent import Rtorrent
from app.schemas.transfer import DownloaderFile, DownloaderTorrent
@@ -19,7 +19,6 @@ from app.foundation import size as size_tools
from app.foundation import temporal as time_tools
from app.foundation import text as text_tools
settings = RuntimeSettingsCompat()
class RtorrentModule(_DownloaderModuleBase[Rtorrent]):
@@ -113,8 +112,8 @@ class RtorrentModule(_DownloaderModuleBase[Rtorrent]):
tag = text_tools.random_string(10)
if label:
tags = label.split(",") + [tag]
elif settings.TORRENT_TAG:
tags = [tag, settings.TORRENT_TAG]
elif get_runtime_setting('TORRENT_TAG'):
tags = [tag, get_runtime_setting('TORRENT_TAG')]
else:
tags = [tag]
# 如果要选择文件则先暂停
@@ -160,14 +159,14 @@ class RtorrentModule(_DownloaderModuleBase[Rtorrent]):
ids=torrent_hash, tag=["已整理"]
)
if (
settings.TORRENT_TAG
and settings.TORRENT_TAG not in torrent_tags
get_runtime_setting('TORRENT_TAG')
and get_runtime_setting('TORRENT_TAG') not in torrent_tags
):
logger.info(
f"给种子 {torrent_hash} 打上标签:{settings.TORRENT_TAG}"
f"给种子 {torrent_hash} 打上标签:{get_runtime_setting('TORRENT_TAG')}"
)
server.set_torrents_tag(
ids=torrent_hash, tags=[settings.TORRENT_TAG]
ids=torrent_hash, tags=[get_runtime_setting('TORRENT_TAG')]
)
return (
downloader or self.get_default_config_name(),
@@ -266,7 +265,7 @@ class RtorrentModule(_DownloaderModuleBase[Rtorrent]):
servers: Dict[str, Rtorrent] = self.get_instances()
ret_torrents = []
query_status = self._normalize_query_status(status)
query_tags = None if include_all_tags else settings.TORRENT_TAG
query_tags = None if include_all_tags else get_runtime_setting('TORRENT_TAG')
def __get_torrent_path(torrent_data: dict) -> Path:
"""
+2 -3
View File
@@ -8,9 +8,8 @@ from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
from slack_sdk import WebClient
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.application.messaging.ingress import forward_message_to_host
from app.domain.context import MediaInfo, Context
from app.domain.metainfo import MetaInfo
@@ -266,7 +265,7 @@ class Slack:
try:
headers = {
"Authorization": f"Bearer {self._oauth_token}",
"User-Agent": settings.USER_AGENT,
"User-Agent": get_runtime_setting('USER_AGENT'),
"Accept": "*/*",
}
resp = RequestUtils(headers=headers, timeout=30).get_res(file_url)
+2 -3
View File
@@ -4,9 +4,8 @@ from urllib.parse import urljoin, urlparse
from lxml import etree
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.domain.context import Context
from app.application.site.query import get_configured_site_query_service
from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module
@@ -146,7 +145,7 @@ class SubtitleModule(_ModuleBase):
request = RequestUtils(
cookies=torrent.site_cookie,
ua=torrent.site_ua,
proxies=settings.PROXY if torrent.site_proxy else None,
proxies=get_runtime_setting('PROXY') if torrent.site_proxy else None,
)
res = request.get_res(torrent.page_url)
if res and res.status_code == 200:
+2 -3
View File
@@ -36,9 +36,8 @@ try:
except ImportError:
from telegramify_markdown.type import ContentTypes, File, Photo, Text # noqa: E402
from app.runtime.settings import RuntimeSettingsCompat # noqa: E402
from app.runtime.settings import get_runtime_setting # noqa: E402
settings = RuntimeSettingsCompat()
from app.domain.context import MediaInfo, Context # noqa: E402
from app.domain.metainfo import MetaInfo # noqa: E402
from app.application.image import ImageHelper # noqa: E402
@@ -124,7 +123,7 @@ class Telegram:
apihelper.API_URL = "https://api.telegram.org/bot{0}/{1}"
apihelper.FILE_URL = "https://api.telegram.org/file/bot{0}/{1}"
# 设置代理
apihelper.proxy = settings.PROXY
apihelper.proxy = get_runtime_setting('PROXY')
# bot
_bot = TeleBot(self._telegram_token, parse_mode=TELEGRAM_PARSE_MODE_MARKDOWN)
# 记录句柄
+10 -11
View File
@@ -1,9 +1,8 @@
from typing import Any, Optional, Tuple, Union
from app.runtime.cache import cached
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.domain.context import (
MusicAlbumInfo,
MusicArtistInfo,
@@ -553,20 +552,20 @@ class TheAudioDbModule(_ModuleBase):
return results
@classmethod
@cached(maxsize=settings.CONF.theaudiodb, ttl=settings.CONF.meta, skip_none=True)
@cached(maxsize=get_runtime_setting('CONF').theaudiodb, ttl=get_runtime_setting('CONF').meta, skip_none=True)
def _request_json(
cls,
endpoint: str,
params: Optional[dict[str, Any]] = None,
) -> Optional[dict[str, Any]]:
"""请求 TheAudioDB V1 JSON 接口并统一处理错误响应。"""
api_key = str(settings.THEAUDIODB_API_KEY or "").strip()
api_key = str(get_runtime_setting('THEAUDIODB_API_KEY') or "").strip()
if not api_key:
logger.warning("TheAudioDB API Key 未配置,跳过请求")
return None
response = RequestUtils(
ua=settings.USER_AGENT,
proxies=settings.PROXY,
ua=get_runtime_setting('USER_AGENT'),
proxies=get_runtime_setting('PROXY'),
timeout=30,
).get_res(
url=f"{cls._base_url}/{api_key}/{endpoint}",
@@ -594,8 +593,8 @@ class TheAudioDbModule(_ModuleBase):
@classmethod
@cached(
maxsize=settings.CONF.theaudiodb,
ttl=settings.CONF.meta,
maxsize=get_runtime_setting('CONF').theaudiodb,
ttl=get_runtime_setting('CONF').meta,
skip_none=True,
shared_key="_request_json",
)
@@ -605,13 +604,13 @@ class TheAudioDbModule(_ModuleBase):
params: Optional[dict[str, Any]] = None,
) -> Optional[dict[str, Any]]:
"""异步请求 TheAudioDB V1 JSON 接口并统一处理错误响应。"""
api_key = str(settings.THEAUDIODB_API_KEY or "").strip()
api_key = str(get_runtime_setting('THEAUDIODB_API_KEY') or "").strip()
if not api_key:
logger.warning("TheAudioDB API Key 未配置,跳过请求")
return None
response = await AsyncRequestUtils(
ua=settings.USER_AGENT,
proxies=settings.PROXY,
ua=get_runtime_setting('USER_AGENT'),
proxies=get_runtime_setting('PROXY'),
timeout=30,
).get_res(
url=f"{cls._base_url}/{api_key}/{endpoint}",
+12 -12
View File
@@ -94,13 +94,13 @@ class TheMovieDbModule(MediaAuxiliaryProviderMixin, _ModuleBase):
"""
测试模块连接性
"""
ret = RequestUtils(ua=get_runtime_setting("NORMAL_USER_AGENT"), proxies=get_runtime_setting("PROXY")).get_res(
f"https://{get_runtime_setting("TMDB_API_DOMAIN")}/3/movie/550?api_key={get_runtime_setting("TMDB_API_KEY")}")
ret = RequestUtils(ua=get_runtime_setting('NORMAL_USER_AGENT'), proxies=get_runtime_setting('PROXY')).get_res(
f"https://{get_runtime_setting('TMDB_API_DOMAIN')}/3/movie/550?api_key={get_runtime_setting('TMDB_API_KEY')}")
if ret and ret.status_code == 200:
return True, ""
elif ret:
return False, f"无法连接 {get_runtime_setting("TMDB_API_DOMAIN")},错误码:{ret.status_code}"
return False, f"{get_runtime_setting("TMDB_API_DOMAIN")} 网络连接失败"
return False, f"无法连接 {get_runtime_setting('TMDB_API_DOMAIN')},错误码:{ret.status_code}"
return False, f"{get_runtime_setting('TMDB_API_DOMAIN')} 网络连接失败"
def init_setting(self) -> Tuple[str, Union[str, bool]]:
pass
@@ -122,7 +122,7 @@ class TheMovieDbModule(MediaAuxiliaryProviderMixin, _ModuleBase):
if not tmdbid and not meta:
return False
selected_source = normalize_media_source(media_source or get_runtime_setting("RECOGNIZE_SOURCE"))
selected_source = normalize_media_source(media_source or get_runtime_setting('RECOGNIZE_SOURCE'))
if meta and not tmdbid and selected_source != MediaSource.TMDB:
return False
@@ -973,7 +973,7 @@ class TheMovieDbModule(MediaAuxiliaryProviderMixin, _ModuleBase):
:param season: 季号
:param episode: 集号
"""
if (mediainfo.scrape_source or get_runtime_setting("SCRAP_SOURCE")) != "themoviedb":
if (mediainfo.scrape_source or get_runtime_setting('SCRAP_SOURCE')) != "themoviedb":
return None
return self.scraper.get_metadata_nfo(meta=meta, mediainfo=mediainfo, season=season, episode=episode)
@@ -985,7 +985,7 @@ class TheMovieDbModule(MediaAuxiliaryProviderMixin, _ModuleBase):
:param season: 季号
:param episode: 集号
"""
if (mediainfo.scrape_source or get_runtime_setting("SCRAP_SOURCE")) != "themoviedb":
if (mediainfo.scrape_source or get_runtime_setting('SCRAP_SOURCE')) != "themoviedb":
return None
return self.scraper.get_metadata_img(mediainfo=mediainfo, season=season, episode=episode)
@@ -1106,7 +1106,7 @@ class TheMovieDbModule(MediaAuxiliaryProviderMixin, _ModuleBase):
:param mediainfo: 媒体信息
:return: None 表示不处理MediaInfo 表示继续处理
"""
if mediainfo.media_source != "themoviedb" and get_runtime_setting("RECOGNIZE_SOURCE") != "themoviedb":
if mediainfo.media_source != "themoviedb" and get_runtime_setting('RECOGNIZE_SOURCE') != "themoviedb":
return None
if not mediainfo.tmdb_id:
return mediainfo
@@ -1147,15 +1147,15 @@ class TheMovieDbModule(MediaAuxiliaryProviderMixin, _ModuleBase):
# 背景图
if not mediainfo.backdrop_path:
if image_path := cls._pick_best_tmdb_image(images.get("backdrops")):
mediainfo.backdrop_path = get_runtime_setting("TMDB_IMAGE_URL")(image_path)
mediainfo.backdrop_path = get_runtime_setting('TMDB_IMAGE_URL')(image_path)
# 标志
if not mediainfo.logo_path:
if image_path := cls._pick_best_tmdb_image(images.get("logos")):
mediainfo.logo_path = get_runtime_setting("TMDB_IMAGE_URL")(image_path)
mediainfo.logo_path = get_runtime_setting('TMDB_IMAGE_URL')(image_path)
# 海报
if not mediainfo.poster_path:
if image_path := cls._pick_best_tmdb_image(images.get("posters")):
mediainfo.poster_path = get_runtime_setting("TMDB_IMAGE_URL")(image_path)
mediainfo.poster_path = get_runtime_setting('TMDB_IMAGE_URL')(image_path)
return mediainfo
def obtain_images(self, mediainfo: MediaInfo) -> Optional[MediaInfo]:
@@ -1245,7 +1245,7 @@ class TheMovieDbModule(MediaAuxiliaryProviderMixin, _ModuleBase):
image_path = seasoninfo.get(image_type.value)
if image_path:
return get_runtime_setting("TMDB_IMAGE_URL")(image_path, image_prefix)
return get_runtime_setting('TMDB_IMAGE_URL')(image_path, image_prefix)
return None
def tmdb_movie_similar(self, tmdbid: int) -> List[MediaInfo]:
+3 -4
View File
@@ -5,9 +5,8 @@ from typing import Union
import ruamel.yaml
from ruamel.yaml import CommentedMap
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.schemas.category import CategoryConfig
from app.foundation.singleton import WeakSingleton
@@ -33,7 +32,7 @@ class CategoryHelper(metaclass=WeakSingleton):
"""
def __init__(self):
self._category_path: Path = settings.CONFIG_PATH / "category.yaml"
self._category_path: Path = get_runtime_setting('CONFIG_PATH') / "category.yaml"
self._categorys = {}
self._movie_categorys = {}
self._tv_categorys = {}
@@ -45,7 +44,7 @@ class CategoryHelper(metaclass=WeakSingleton):
"""
try:
if not self._category_path.exists():
shutil.copy(settings.INNER_CONFIG_PATH / "category.yaml", self._category_path)
shutil.copy(get_runtime_setting('INNER_CONFIG_PATH') / "category.yaml", self._category_path)
with open(self._category_path, mode='r', encoding='utf-8', errors='replace') as f:
try:
yaml_loader = ruamel.yaml.YAML()
+11 -12
View File
@@ -2,9 +2,8 @@ from pathlib import Path
from typing import Optional, Tuple
from xml.dom import minidom
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.domain.context import MediaInfo
from app.domain.meta.metabase import MetaBase
from app.schemas.types import MediaType
@@ -22,14 +21,14 @@ class TmdbScraper:
获取元数据TMDB Api
"""
if not self._meta_tmdb:
self._meta_tmdb = TmdbApi(language=settings.TMDB_LOCALE)
self._meta_tmdb = TmdbApi(language=get_runtime_setting('TMDB_LOCALE'))
return self._meta_tmdb
def original_tmdb(self, mediainfo: Optional[MediaInfo] = None):
"""
获取图片TMDB Api
"""
if settings.TMDB_SCRAP_ORIGINAL_IMAGE and mediainfo:
if get_runtime_setting('TMDB_SCRAP_ORIGINAL_IMAGE') and mediainfo:
return TmdbApi(language=mediainfo.original_language)
return self.default_tmdb
@@ -116,7 +115,7 @@ class TmdbScraper:
# TMDB集still图片
ext = Path(still_path).suffix
still_name = f"episode-thumb{ext}"
still_url = settings.TMDB_IMAGE_URL(still_path)
still_url = get_runtime_setting('TMDB_IMAGE_URL')(still_path)
images[still_name] = still_url
else:
# 季的图片
@@ -144,14 +143,14 @@ class TmdbScraper:
images[image_name] = attr_value
# 替换原语言Poster
if settings.TMDB_SCRAP_ORIGINAL_IMAGE:
if get_runtime_setting('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 = settings.TMDB_IMAGE_URL(attr_value)
image_url = get_runtime_setting('TMDB_IMAGE_URL')(attr_value)
image_name = (
attr_name.replace("_path", "") + Path(image_url).suffix
)
@@ -181,11 +180,11 @@ class TmdbScraper:
if not mediainfo.poster_path:
poster_path = self.__pick_best_image_path(image_info.get("posters"))
if poster_path:
mediainfo.poster_path = settings.TMDB_IMAGE_URL(poster_path)
mediainfo.poster_path = get_runtime_setting('TMDB_IMAGE_URL')(poster_path)
if not mediainfo.backdrop_path:
backdrop_path = self.__pick_best_image_path(image_info.get("backdrops"))
if backdrop_path:
mediainfo.backdrop_path = settings.TMDB_IMAGE_URL(backdrop_path)
mediainfo.backdrop_path = get_runtime_setting('TMDB_IMAGE_URL')(backdrop_path)
@staticmethod
def __pick_best_image_path(images: list) -> Optional[str]:
@@ -215,7 +214,7 @@ class TmdbScraper:
# 后缀
ext = Path(poster_path).suffix
# URL
url = settings.TMDB_IMAGE_URL(poster_path)
url = get_runtime_setting('TMDB_IMAGE_URL')(poster_path)
# S0海报格式不同
if season == 0:
image_name = f"season-specials-poster{ext}"
@@ -286,7 +285,7 @@ class TmdbScraper:
DomUtils.add_node(doc, xactor, "tmdbid", actor.get("id") or "")
if profile_path := actor.get("profile_path"):
DomUtils.add_node(
doc, xactor, "thumb", settings.TMDB_IMAGE_URL(profile_path)
doc, xactor, "thumb", get_runtime_setting('TMDB_IMAGE_URL')(profile_path)
)
DomUtils.add_node(
doc,
@@ -453,7 +452,7 @@ class TmdbScraper:
DomUtils.add_node(doc, xactor, "tmdbid", actor.get("id") or "")
if profile_path := actor.get("profile_path"):
DomUtils.add_node(
doc, xactor, "thumb", settings.TMDB_IMAGE_URL(profile_path)
doc, xactor, "thumb", get_runtime_setting('TMDB_IMAGE_URL')(profile_path)
)
DomUtils.add_node(
doc,
+9 -10
View File
@@ -6,9 +6,8 @@ from time import time
from typing import Any, Optional
from app.runtime.cache import FileCache, TTLCache
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.domain.meta.metabase import MetaBase
from app.runtime.log import logger
from app.schemas.types import MediaSource, MediaType
@@ -32,8 +31,8 @@ class TmdbCache(metaclass=WeakSingleton):
"""
def __init__(self):
"""初始化 TMDB 识别缓存并恢复未过期的持久化数据。"""
self.maxsize = settings.CONF.tmdb
self.ttl = settings.CONF.meta
self.maxsize = get_runtime_setting('CONF').tmdb
self.ttl = get_runtime_setting('CONF').meta
self.region = "__tmdb_cache__"
self._cache = TTLCache(region=self.region, maxsize=self.maxsize, ttl=self.ttl)
self._expires_at: dict[str, float] = {}
@@ -42,8 +41,8 @@ class TmdbCache(metaclass=WeakSingleton):
self._legacy_file_cache = None
self._legacy_cache_found = False
if not self._cache.is_redis():
self._file_cache = FileCache(base=settings.CACHE_PATH, ttl=self.ttl)
self._legacy_file_cache = FileCache(base=settings.TEMP_PATH.parent, ttl=self.ttl)
self._file_cache = FileCache(base=get_runtime_setting('CACHE_PATH'), ttl=self.ttl)
self._legacy_file_cache = FileCache(base=get_runtime_setting('TEMP_PATH').parent, ttl=self.ttl)
self._restore()
def _restore(self) -> None:
@@ -53,7 +52,7 @@ class TmdbCache(metaclass=WeakSingleton):
if not content:
content = self._legacy_file_cache.get(
self.region,
region=settings.TEMP_PATH.name,
region=get_runtime_setting('TEMP_PATH').name,
)
if content:
self._legacy_cache_found = True
@@ -146,7 +145,7 @@ class TmdbCache(metaclass=WeakSingleton):
获取缓存KEY
"""
media_id = meta.media_id if meta.media_source == MediaSource.TMDB else None
return f"[{meta.type.value if meta.type else '未知'}][{settings.TMDB_LOCALE}]{media_id or meta.name}-{meta.year}-{meta.begin_season}"
return f"[{meta.type.value if meta.type else '未知'}][{get_runtime_setting('TMDB_LOCALE')}]{media_id or meta.name}-{meta.year}-{meta.begin_season}"
@staticmethod
def __is_type_conflicted(meta: MetaBase, media_type: Any, tmdb_id: Any) -> bool:
@@ -263,7 +262,7 @@ class TmdbCache(metaclass=WeakSingleton):
# 负识别缓存使用独立的短 TTL:故障期间「合法 JSON 但结果为空」会被
# 记为未识别,若按完整有效期固化,故障自愈后同名仍会被判无法识别;
# 短过期让恢复后可重新识别,真不存在的条目过期后重新确认一次即可
self._set(key, {"id": 0}, ttl=settings.EMPTY_RESULT_CACHE_TTL)
self._set(key, {"id": 0}, ttl=get_runtime_setting('EMPTY_RESULT_CACHE_TTL'))
def save(self, force: bool = False) -> None:
"""
@@ -314,7 +313,7 @@ class TmdbCache(metaclass=WeakSingleton):
if self._legacy_cache_found:
self._legacy_file_cache.delete(
self.region,
region=settings.TEMP_PATH.name,
region=get_runtime_setting('TEMP_PATH').name,
)
self._legacy_cache_found = False
self._dirty = False
+2 -3
View File
@@ -2,9 +2,8 @@ import re
import traceback
from typing import Optional, List
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.schemas.types import MediaType
from app.foundation import text as text_tools
@@ -1279,7 +1278,7 @@ class TmdbApi:
"""
languages = []
for language in (
settings.TMDB_LOCALE,
get_runtime_setting('TMDB_LOCALE'),
"en",
None,
original_language,
@@ -1,7 +1,6 @@
from app.runtime.cache import cached
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from ..tmdb import TMDb
try:
@@ -16,7 +15,7 @@ class Discover(TMDb):
"tv": "/discover/tv"
}
@cached(maxsize=1, ttl=43200, empty_ttl=settings.EMPTY_RESULT_CACHE_TTL)
@cached(maxsize=1, ttl=43200, empty_ttl=get_runtime_setting('EMPTY_RESULT_CACHE_TTL'))
def discover_movies(self, params_tuple):
"""
Discover movies by different types of data like average rating, number of votes, genres and certifications.
@@ -26,7 +25,7 @@ class Discover(TMDb):
params = dict(params_tuple)
return self._request_obj(self._urls["movies"], urlencode(params), key="results", call_cached=False)
@cached(maxsize=1, ttl=43200, empty_ttl=settings.EMPTY_RESULT_CACHE_TTL)
@cached(maxsize=1, ttl=43200, empty_ttl=get_runtime_setting('EMPTY_RESULT_CACHE_TTL'))
def discover_tv_shows(self, params_tuple):
"""
Discover TV shows by different types of data like average rating, number of votes, genres,
@@ -36,7 +35,7 @@ class Discover(TMDb):
"""
return self._request_obj(self._urls["tv"], urlencode(params_tuple), key="results", call_cached=False)
@cached(maxsize=1, ttl=43200, empty_ttl=settings.EMPTY_RESULT_CACHE_TTL)
@cached(maxsize=1, ttl=43200, empty_ttl=get_runtime_setting('EMPTY_RESULT_CACHE_TTL'))
async def async_discover_movies(self, params_tuple):
"""
Discover movies by different types of data like average rating, number of votes, genres and certifications.异步版本
@@ -46,7 +45,7 @@ class Discover(TMDb):
params = dict(params_tuple)
return await self._async_request_obj(self._urls["movies"], urlencode(params), key="results", call_cached=False)
@cached(maxsize=1, ttl=43200, empty_ttl=settings.EMPTY_RESULT_CACHE_TTL)
@cached(maxsize=1, ttl=43200, empty_ttl=get_runtime_setting('EMPTY_RESULT_CACHE_TTL'))
async def async_discover_tv_shows(self, params_tuple):
"""
Discover TV shows by different types of data like average rating, number of votes, genres,
+12 -13
View File
@@ -10,9 +10,8 @@ import requests
import requests.exceptions
from app.runtime.cache import cached, fresh, async_fresh
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.settings import get_runtime_setting
settings = RuntimeSettingsCompat()
from app.adapters.network.http import RequestUtils, AsyncRequestUtils
from .exceptions import TMDbException, TMDbConnectionError
@@ -44,7 +43,7 @@ def _is_empty_result_snapshot(snapshot) -> bool:
判断响应快照是否为空结果列表/搜索类接口的 results 为空列表
这类快照结构合法但无业务内容常由代理瞬时故障产生不能靠 skip_none/skip_empty
识别快照本身是非空字典需单独谓词判定后按 settings.EMPTY_RESULT_CACHE_TTL
识别快照本身是非空字典需单独谓词判定后按 get_runtime_setting('EMPTY_RESULT_CACHE_TTL')
TTL 缓存详情类接口无 results 字段不属于空结果
"""
if not isinstance(snapshot, dict):
@@ -60,13 +59,13 @@ class TMDb(object):
_RESPONSE_SNAPSHOT_MARKER = "__mp_tmdb_response_snapshot__"
def __init__(self, session=None, language=None):
self._api_key = settings.TMDB_API_KEY
self._language = language or settings.TMDB_LOCALE or "en-US"
self._api_key = get_runtime_setting('TMDB_API_KEY')
self._language = language or get_runtime_setting('TMDB_LOCALE') or "en-US"
self._session_id = None
self._session = session
self._wait_on_rate_limit = True
self._proxies = settings.PROXY
self._domain = settings.TMDB_API_DOMAIN
self._proxies = get_runtime_setting('PROXY')
self._domain = get_runtime_setting('TMDB_API_DOMAIN')
self._page = None
self._total_results = None
self._total_pages = None
@@ -76,7 +75,7 @@ class TMDb(object):
# TMDB 在部分代理和运营商链路下的 HTTP/2 长连接偶发卡死,识别路径优先保证稳定性。
self._async_req = AsyncRequestUtils(
ua=settings.NORMAL_USER_AGENT,
ua=get_runtime_setting('NORMAL_USER_AGENT'),
proxies=self.proxies,
http2=False,
)
@@ -91,7 +90,7 @@ class TMDb(object):
"""
self._session = session or requests.Session()
self._req = RequestUtils(
ua=settings.NORMAL_USER_AGENT,
ua=get_runtime_setting('NORMAL_USER_AGENT'),
session=self._session,
proxies=self.proxies,
)
@@ -175,9 +174,9 @@ class TMDb(object):
def wait_on_rate_limit(self, wait_on_rate_limit):
self._wait_on_rate_limit = bool(wait_on_rate_limit)
@cached(maxsize=settings.CONF.tmdb, ttl=settings.CONF.meta, skip_none=True,
@cached(maxsize=get_runtime_setting('CONF').tmdb, ttl=get_runtime_setting('CONF').meta, skip_none=True,
skip_if=_is_business_failure_snapshot,
empty_ttl=settings.EMPTY_RESULT_CACHE_TTL, empty_if=_is_empty_result_snapshot)
empty_ttl=get_runtime_setting('EMPTY_RESULT_CACHE_TTL'), empty_if=_is_empty_result_snapshot)
def request(self, method, url, data, json, **kwargs):
req = self._request_once(method, url, data, json)
if req is None and method == "GET" and self._owns_session:
@@ -201,9 +200,9 @@ class TMDb(object):
return self._req.get_res(url, params=data, json=json)
return self._req.post_res(url, data=data, json=json)
@cached(maxsize=settings.CONF.tmdb, ttl=settings.CONF.meta, skip_none=True,
@cached(maxsize=get_runtime_setting('CONF').tmdb, ttl=get_runtime_setting('CONF').meta, skip_none=True,
skip_if=_is_business_failure_snapshot,
empty_ttl=settings.EMPTY_RESULT_CACHE_TTL, empty_if=_is_empty_result_snapshot)
empty_ttl=get_runtime_setting('EMPTY_RESULT_CACHE_TTL'), empty_if=_is_empty_result_snapshot)
async def async_request(self, method, url, data, json, **kwargs):
req = await self._async_request_once(method, url, data, json)
if req is None:
+4 -4
View File
@@ -38,11 +38,11 @@ class TheTvDbModule(_ModuleBase):
action = "刷新" if is_retry else "创建"
logger.info(f"开始{action}TVDB登录会话...")
try:
if not get_runtime_setting("TVDB_V4_API_KEY"):
if not get_runtime_setting('TVDB_V4_API_KEY'):
raise ConnectionError("TVDB API Key 未配置,无法初始化会话。")
self.tvdb = tvdb_v4_official.TVDB(apikey=get_runtime_setting("TVDB_V4_API_KEY"),
pin=get_runtime_setting("TVDB_V4_API_PIN"),
proxy=get_runtime_setting("PROXY"),
self.tvdb = tvdb_v4_official.TVDB(apikey=get_runtime_setting('TVDB_V4_API_KEY'),
pin=get_runtime_setting('TVDB_V4_API_PIN'),
proxy=get_runtime_setting('PROXY'),
timeout=self.__timeout)
if self.tvdb:
logger.info(f"TVDB登录会话{action}成功。")

Some files were not shown because too many files have changed in this diff Show More