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

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