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