Files
LitePan/config.py
T
KingandCursor 463045a4f1 新增123云盘Open与OneDrive驱动
- 接入123云盘Open与OneDrive基础驱动能力,补充OAuth映射与图标资源
- 优化账号连通性测试和驱动实例复用,避免临时测试串用旧驱动
- 增加上传速度显示,上传文件夹跳过空文件和常见系统垃圾文件
- 优化前台删除、重命名、移动后的列表刷新体验
- 调整添加账号弹窗、STRM空状态和深色模式细节

Co-authored-by: Cursor <[email protected]>
2026-06-05 21:10:12 +08:00

716 lines
26 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""LitePan 配置入口:定义默认值 + 数据库为准的 ConfigManager,兼容同步与异步访问。"""
import secrets
import os
import json
from typing import Any, Dict
from pathlib import Path
APP_NAME = "LitePan"
APP_VERSION = "0.2.0-beta"
try:
# 日志模块延迟导入,避免应用启动早期的循环依赖
from core.log_manager import get_writer, LogModule
_log_available = True
except ImportError:
_log_available = False
class Settings:
# 基础服务参数(不入库,进程内使用)
WEB_HOST: str = '0.0.0.0'
WEB_PORT: int = 5211
DATABASE_PATH: str = 'data/litepan.db'
SECRET_KEY: str = "" # 由 _get_or_create_secret_key 生成
LOG_LEVEL: str = "INFO"
LOG_RETENTION_DAYS: int = 10
# 系统/安全(入库,可在后台调整)
ADMIN_USERNAME: str = "admin"
ADMIN_PASSWORD: str = "admin" # 明文默认值,落库前会做 hash
SESSION_TIMEOUT: int = 7200
OAUTH_SERVER_URL: str = "https://oauth.litepan.top"
PUBLIC_INDEX_ENABLED: bool = True
INDEX_ACCOUNT_SWITCH_MODE: str = "dropdown"
ADMIN_HOME_RETURN_MODE: str = "top_icon"
STRM_BASE_URL: str = ""
STRM_TOKEN: str = "" # 首次落库时自动生成
STRM_LINK_FORMAT: str = "v1"
STRM_DEFAULT_SCAN_INTERVAL: int = 120
STRM_TASK_CONCURRENCY: int = 3
UPLOAD_TASK_CONCURRENCY: int = 3
AUTH_ACTIVE_REFRESH_ENABLED: bool = True
# 缓存
CACHE_ENABLED: bool = True
CACHE_TTL: int = 1800
CACHE_PERSISTENCE_ENABLED: bool = True
CACHE_PERSISTENCE_INTERVAL_SECONDS: int = 600
CACHE_MAX_ITEMS: int = 10000
CACHE_MEMORY_LIMIT_MB: int = 512
# WebDAV
WEBDAV_ENABLED: bool = True
WEBDAV_SMART_CHUNK_ENABLED: bool = True
WEBDAV_CHUNK_SIZE: int = 262144
WEBDAV_CACHE_ENABLED: bool = True
# 性能 / 前端
MAX_CONCURRENT_REQUESTS: int = 10
REQUEST_TIMEOUT: int = 30
THEME: str = "light"
ITEMS_PER_PAGE: int = 50
# 缓存保持
CACHE_RETENTION_DEFAULT_API_INTERVAL: int = 200
CACHE_RETENTION_DEFAULT_REFRESH_INTERVAL: int = 60
STRM_DEFAULT_API_INTERVAL: int = 200
# 媒体整理 — 代理
MO_PROXY_ENABLED: bool = False
MO_PROXY_URL: str = ""
MO_PROXY_USERNAME: str = ""
MO_PROXY_PASSWORD: str = ""
# 媒体整理 — TMDB
MO_TMDB_API_KEY: str = ""
MO_TMDB_LANGUAGE: str = "zh-CN"
# 媒体整理 — 节流
MO_API_REQUEST_INTERVAL_MS: int = 300
MO_FFPROBE_REQUEST_INTERVAL_MS: int = 3000
MO_TMDB_REQUEST_INTERVAL_MS: int = 250
# 媒体整理 — 执行
MO_FFPROBE_CONCURRENCY: int = 2
MO_FFPROBE_TIMEOUT_SECONDS: int = 30
MO_MIN_CONFIDENCE_THRESHOLD: float = 0.5
# 媒体整理 — 文件类型
MO_FILE_EXTENSIONS: str = "mkv;mp4;avi;ts;mov;wmv;iso;m2ts;rmvb;flv;m4v;webm"
MO_METADATA_EXTENSIONS: str = "nfo;ass;ssa;srt;sub;idx;sup;vtt;jpg;jpeg;png;webp;bmp"
# 媒体整理 — 媒体信息标签排序
MO_MEDIA_TAG_ORDER: str = '["screen_size","video_codec","audio_codec","audio_channels"]'
MO_ALIGN_MEDIA_TAGS: bool = False
MO_MAX_WORKS_PER_RUN: int = 50
MO_OVERWRITE_EXISTING: bool = False
@classmethod
def get_default_value(cls, key: str) -> Any:
return getattr(cls, key.upper(), None)
@classmethod
def get_config_metadata(cls) -> Dict[str, Dict[str, Any]]:
return {
"admin_username": {
"type": "string",
"description": "管理员用户名",
"category": "security",
"default": cls.ADMIN_USERNAME
},
"admin_password": {
"type": "password",
"description": "管理员密码",
"category": "security",
"sensitive": True,
"default": cls.ADMIN_PASSWORD
},
"session_timeout": {
"type": "integer",
"description": "会话超时时间",
"category": "security",
"unit": "小时",
"min": 0.5,
"max": 24,
"default": cls.SESSION_TIMEOUT / 3600 # 展示单位:小时
},
"oauth_server_url": {
"type": "string",
"description": "OAuth代理服务地址",
"category": "system",
"default": cls.OAUTH_SERVER_URL
},
"public_index_enabled": {
"type": "boolean",
"description": "是否允许匿名访问前台文件列表",
"category": "system",
"default": cls.PUBLIC_INDEX_ENABLED
},
"index_account_switch_mode": {
"type": "string",
"description": "首页账号切换方式",
"category": "system",
"default": cls.INDEX_ACCOUNT_SWITCH_MODE
},
"admin_home_return_mode": {
"type": "string",
"description": "后台返回首页入口方式",
"category": "system",
"default": cls.ADMIN_HOME_RETURN_MODE
},
"strm_base_url": {
"type": "string",
"description": "STRM播放地址基址",
"category": "system",
"default": cls.STRM_BASE_URL
},
"strm_token": {
"type": "password",
"description": "STRM访问令牌",
"category": "system",
"sensitive": True,
"default": cls.STRM_TOKEN
},
"strm_link_format": {
"type": "string",
"description": "STRM链接格式",
"category": "system",
"default": cls.STRM_LINK_FORMAT
},
"strm_default_scan_interval": {
"type": "integer",
"description": "STRM默认扫描间隔",
"category": "system",
"unit": "分钟",
"min": 1,
"max": 1440,
"default": cls.STRM_DEFAULT_SCAN_INTERVAL
},
"strm_task_concurrency": {
"type": "integer",
"description": "STRM任务并发数",
"category": "system",
"unit": "个",
"min": 1,
"max": 10,
"default": cls.STRM_TASK_CONCURRENCY
},
"upload_task_concurrency": {
"type": "integer",
"description": "上传任务并发数",
"category": "system",
"unit": "个",
"min": 1,
"max": 5,
"default": cls.UPLOAD_TASK_CONCURRENCY
},
"auth_active_refresh_enabled": {
"type": "boolean",
"description": "是否启用主动认证刷新",
"category": "system",
"default": cls.AUTH_ACTIVE_REFRESH_ENABLED
},
"cache_enabled": {
"type": "boolean",
"description": "是否启用缓存",
"category": "performance",
"default": cls.CACHE_ENABLED
},
"cache_ttl": {
"type": "integer",
"description": "缓存过期时间",
"category": "performance",
"unit": "分钟",
"min": 1,
"max": 1440,
"default": cls.CACHE_TTL / 60 # 展示单位:分钟
},
"cache_persistence_enabled": {
"type": "boolean",
"description": "是否启用缓存持久化",
"category": "performance",
"default": cls.CACHE_PERSISTENCE_ENABLED
},
"cache_persistence_interval_seconds": {
"type": "integer",
"description": "缓存持久化快照间隔",
"category": "performance",
"unit": "分钟",
"min": 1,
"max": 1440,
"default": cls.CACHE_PERSISTENCE_INTERVAL_SECONDS // 60
},
"cache_max_items": {
"type": "integer",
"description": "缓存条目最大数量",
"category": "performance",
"unit": "条",
"min": 1000,
"max": 1000000,
"default": cls.CACHE_MAX_ITEMS
},
"cache_memory_limit_mb": {
"type": "integer",
"description": "缓存内存上限(超80%触发淘汰)",
"category": "performance",
"unit": "MB",
"min": 64,
"max": 16384,
"default": cls.CACHE_MEMORY_LIMIT_MB
},
"webdav_enabled": {
"type": "boolean",
"description": "是否启用WebDAV服务",
"category": "webdav",
"default": cls.WEBDAV_ENABLED
},
"webdav_smart_chunk_enabled": {
"type": "boolean",
"description": "是否启用智能块大小",
"category": "webdav",
"default": cls.WEBDAV_SMART_CHUNK_ENABLED
},
"webdav_chunk_size": {
"type": "integer",
"description": "固定块大小",
"category": "webdav",
"unit": "KB",
"min": 64,
"max": 8192,
"default": cls.WEBDAV_CHUNK_SIZE // 1024 # 展示单位:KB
},
"webdav_cache_enabled": {
"type": "boolean",
"description": "是否启用WebDAV缓存优化",
"category": "webdav",
"default": cls.WEBDAV_CACHE_ENABLED
},
"theme": {
"type": "select",
"description": "界面主题",
"category": "ui",
"options": [
{"value": "light", "label": "浅色主题"},
{"value": "dark", "label": "深色主题"},
{"value": "auto", "label": "跟随系统"}
],
"default": cls.THEME
},
"items_per_page": {
"type": "integer",
"description": "每页显示项目数",
"category": "ui",
"min": 10,
"max": 200,
"default": cls.ITEMS_PER_PAGE
},
"cache_retention_default_api_interval": {
"type": "integer",
"description": "默认API请求间隔",
"category": "cache_retention",
"unit": "毫秒",
"min": 50,
"max": 10000,
"default": cls.CACHE_RETENTION_DEFAULT_API_INTERVAL
},
"cache_retention_default_refresh_interval": {
"type": "integer",
"description": "默认刷新间隔",
"category": "cache_retention",
"unit": "分钟",
"min": 5,
"max": 1440,
"default": cls.CACHE_RETENTION_DEFAULT_REFRESH_INTERVAL
},
"mo_proxy_enabled": {
"type": "boolean", "description": "启用代理", "category": "media_organize", "default": cls.MO_PROXY_ENABLED
},
"mo_proxy_url": {
"type": "string", "description": "代理地址", "category": "media_organize", "default": cls.MO_PROXY_URL
},
"mo_proxy_username": {
"type": "string", "description": "代理用户名", "category": "media_organize", "default": cls.MO_PROXY_USERNAME
},
"mo_proxy_password": {
"type": "password", "description": "代理密码", "category": "media_organize", "sensitive": True, "default": cls.MO_PROXY_PASSWORD
},
"mo_tmdb_api_key": {
"type": "password", "description": "TMDB API Key", "category": "media_organize", "sensitive": True, "default": cls.MO_TMDB_API_KEY
},
"mo_tmdb_language": {
"type": "string", "description": "TMDB 搜索语言", "category": "media_organize", "default": cls.MO_TMDB_LANGUAGE
},
"mo_api_request_interval_ms": {
"type": "integer", "description": "API额外补偿间隔", "category": "media_organize", "unit": "毫秒", "min": 50, "max": 10000, "default": cls.MO_API_REQUEST_INTERVAL_MS
},
"mo_ffprobe_request_interval_ms": {
"type": "integer", "description": "ffprobe请求间隔", "category": "media_organize", "unit": "毫秒", "min": 500, "max": 30000, "default": cls.MO_FFPROBE_REQUEST_INTERVAL_MS
},
"mo_tmdb_request_interval_ms": {
"type": "integer", "description": "TMDB请求间隔", "category": "media_organize", "unit": "毫秒", "min": 100, "max": 5000, "default": cls.MO_TMDB_REQUEST_INTERVAL_MS
},
"mo_ffprobe_concurrency": {
"type": "integer", "description": "ffprobe并发数", "category": "media_organize", "min": 1, "max": 10, "default": cls.MO_FFPROBE_CONCURRENCY
},
"mo_ffprobe_timeout_seconds": {
"type": "integer", "description": "ffprobe超时", "category": "media_organize", "unit": "秒", "min": 5, "max": 120, "default": cls.MO_FFPROBE_TIMEOUT_SECONDS
},
"mo_min_confidence_threshold": {
"type": "number", "description": "最低置信度阈值", "category": "media_organize", "min": 0.1, "max": 1.0, "default": cls.MO_MIN_CONFIDENCE_THRESHOLD
},
"mo_file_extensions": {
"type": "string", "description": "媒体文件扩展名", "category": "media_organize", "default": cls.MO_FILE_EXTENSIONS
},
"mo_metadata_extensions": {
"type": "string", "description": "元数据文件扩展名", "category": "media_organize", "default": cls.MO_METADATA_EXTENSIONS
},
"mo_media_tag_order": {
"type": "string", "description": "媒体信息标签排序", "category": "media_organize", "default": cls.MO_MEDIA_TAG_ORDER
},
"mo_align_media_tags": {
"type": "boolean", "description": "强迫症模式:同后缀文件保持媒体信息一致", "category": "media_organize", "default": cls.MO_ALIGN_MEDIA_TAGS
},
"mo_max_works_per_run": {
"type": "integer", "description": "每次最多整理作品数(0=不限制)", "category": "media_organize", "min": 0, "max": 10000, "default": cls.MO_MAX_WORKS_PER_RUN
},
"mo_overwrite_existing": {
"type": "boolean", "description": "同名冲突时覆盖(默认跳过)", "category": "media_organize", "default": cls.MO_OVERWRITE_EXISTING
},
}
@classmethod
def get_categories(cls) -> Dict[str, str]:
return {
"system": "系统设置",
"security": "安全设置",
"performance": "性能设置",
"webdav": "WebDAV设置",
"ui": "界面设置",
"cache_retention": "缓存保持设置",
"media_organize": "媒体整理设置",
}
@classmethod
def _get_or_create_secret_key(cls):
"""SECRET_KEY 优先级:环境变量 > data/.secret_key 文件 > 随机生成并落盘。"""
secret_file = Path('data/.secret_key')
env_secret = os.getenv('SECRET_KEY')
if env_secret:
return env_secret
if secret_file.exists():
try:
key = secret_file.read_text(encoding='utf-8').strip()
if key:
return key
except Exception as e:
print(f"读取密钥文件失败: {e}")
new_key = secrets.token_urlsafe(64)
try:
secret_file.parent.mkdir(exist_ok=True)
secret_file.write_text(new_key, encoding='utf-8')
secret_file.chmod(0o600) # 只允许所有者读写
# 日志系统此时可能还未启动,手动打印一条提示
from datetime import datetime
timestamp = datetime.now().strftime('%H:%M:%S')
print(f"[{timestamp}] ℹ️ 配置 | 新的SECRET_KEY已生成并保存")
except Exception as e:
print(f"保存密钥文件失败: {e}")
return new_key
class ConfigManager:
"""数据库优先、默认值兜底。内部对 sync 入口做兼容,避免在事件循环中误用 asyncio.run。"""
def __init__(self):
self._cache: Dict[str, Any] = {}
self._initialized = False
def initialize_sync(self):
if self._initialized:
return
self._load_from_db_sync()
self._initialized = True
def _load_from_db_sync(self):
try:
import asyncio
try:
loop = asyncio.get_running_loop()
task = loop.create_task(self._load_from_db_async())
loop.run_until_complete(task)
except RuntimeError:
asyncio.run(self._load_from_db_async())
except Exception as e:
print(f"从数据库加载配置失败: {e}")
async def _load_from_db_sync_async(self):
try:
await self._load_from_db_async()
except Exception as e:
print(f"从数据库加载配置失败: {e}")
def initialize_sync_force(self):
if self._initialized:
return
# 启动阶段强制走同步分支,避免被事件循环假状态干扰
print("强制使用同步配置初始化")
self._load_from_db_sync()
self._initialized = True
async def initialize_async(self):
if self._initialized:
return
await self._load_from_db_async()
self._initialized = True
async def _load_from_db_async(self):
try:
from database.db import get_db
db_conn = await get_db()
async with db_conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='configs'") as cursor:
result = await cursor.fetchone()
if not result:
# 首启:建表 + 写默认值
await self._create_config_table_async(db_conn)
await self._init_default_configs_async()
return
async with db_conn.execute("SELECT key, value FROM configs") as cursor:
rows = await cursor.fetchall()
for row in rows:
key, value_str = row
try:
self._cache[key] = json.loads(value_str)
except json.JSONDecodeError:
self._cache[key] = value_str
await self._init_default_configs_async()
except Exception as e:
print(f"从数据库加载配置失败: {e}")
async def _create_config_table_async(self, db_conn):
try:
# noinspection SqlNoDataSourceInspection,SqlDialectInspection
await db_conn.execute("""
CREATE TABLE IF NOT EXISTS configs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT NOT NULL UNIQUE,
value TEXT NOT NULL,
description TEXT
)
""")
await db_conn.commit()
except Exception as e:
if _log_available:
config_log = get_writer(LogModule.CONFIG)
config_log.error(f"创建配置表失败: {e}")
else:
print(f"创建配置表失败: {e}")
def get(self, key: str) -> Any:
if not self._initialized:
import asyncio
try:
asyncio.get_running_loop()
# 事件循环中禁止同步初始化,直接返回默认值避免死锁
print("检测到事件循环,使用默认配置值")
return Settings.get_default_value(key)
except RuntimeError:
self.initialize_sync()
if key in self._cache:
return self._cache[key]
return Settings.get_default_value(key)
async def get_async(self, key: str) -> Any:
if not self._initialized:
await self.initialize_async()
if key in self._cache:
return self._cache[key]
return Settings.get_default_value(key)
def set(self, key: str, value: Any) -> bool:
if not self._initialized:
self.initialize_sync()
try:
import asyncio
try:
loop = asyncio.get_running_loop()
task = loop.create_task(self._set_async(key, value))
loop.run_until_complete(task)
except RuntimeError:
asyncio.run(self._set_async(key, value))
return True
except Exception as e:
print(f"设置配置失败: {key}={value}, 错误: {e}")
return False
async def set_async(self, key: str, value: Any) -> bool:
if not self._initialized:
await self.initialize_async()
await self._set_async(key, value)
return True
async def _set_async(self, key: str, value: Any) -> bool:
from database.db import get_db
db_conn = await get_db()
if not hasattr(self, '_table_created'):
await self._create_config_table_async(db_conn)
self._table_created = True
value_str = json.dumps(value)
await db_conn.execute(
"INSERT OR REPLACE INTO configs (key, value) VALUES (?, ?)",
(key, value_str)
)
await db_conn.commit()
self._cache[key] = value
return True
def reset_to_default(self, key: str) -> bool:
if not self._initialized:
self.initialize_sync()
try:
import asyncio
try:
loop = asyncio.get_running_loop()
task = loop.create_task(self._reset_to_default_async(key))
loop.run_until_complete(task)
except RuntimeError:
asyncio.run(self._reset_to_default_async(key))
return True
except Exception as e:
print(f"重置配置失败: {key}, 错误: {e}")
return False
async def _reset_to_default_async(self, key: str) -> bool:
try:
from database.db import get_db
db_conn = await get_db()
if not hasattr(self, '_table_created'):
await self._create_config_table_async(db_conn)
self._table_created = True
await db_conn.execute("DELETE FROM configs WHERE key = ?", (key,))
await db_conn.commit()
if key in self._cache:
del self._cache[key]
return True
except Exception as e:
print(f"重置配置失败: {key}, 错误: {e}")
return False
async def _init_default_configs_async(self):
try:
from database.db import get_db
db_conn = await get_db()
metadata = Settings.get_config_metadata()
for key, meta in metadata.items():
default_value = Settings.get_default_value(key)
if default_value is not None:
# strm_token 默认空,首启要生成一次随机值
if key == "strm_token" and not str(default_value):
default_value = secrets.token_urlsafe(32)
async with db_conn.execute("SELECT key FROM configs WHERE key = ?", (key,)) as cursor:
exists = await cursor.fetchone()
if not exists:
value_str = json.dumps(default_value)
await db_conn.execute(
"INSERT INTO configs (key, value) VALUES (?, ?)",
(key, value_str)
)
self._cache[key] = default_value
await db_conn.commit()
if _log_available:
config_log = get_writer(LogModule.CONFIG)
config_log.debug("默认配置初始化完成")
else:
print("✓ 默认配置初始化完成")
except Exception as e:
print(f"初始化默认配置失败: {e}")
def get_all_with_metadata(self) -> list:
if not self._initialized:
self.initialize_sync()
metadata = Settings.get_config_metadata()
result = []
for key, meta in metadata.items():
current_value = self.get(key)
default_value = meta["default"]
config_item = {
"key": key,
"current_value": current_value,
"default_value": default_value,
"is_default": current_value == default_value,
**meta
}
# sensitive 字段只回显掩码,管理员用户名例外
if meta.get("sensitive") and key != "admin_username":
config_item["current_value"] = "******"
result.append(config_item)
return result
def init_default_configs(self):
if not self._initialized:
self.initialize_sync()
try:
import asyncio
try:
loop = asyncio.get_running_loop()
task = loop.create_task(self._init_default_configs_async())
loop.run_until_complete(task)
except RuntimeError:
asyncio.run(self._init_default_configs_async())
except Exception as e:
print(f"初始化默认配置失败: {e}")
settings = Settings()
config_manager = ConfigManager()
def get_oauth_server_url() -> str:
value = config_manager.get("oauth_server_url")
if not value:
value = Settings.OAUTH_SERVER_URL
return str(value).rstrip("/")
Settings.SECRET_KEY = Settings._get_or_create_secret_key()
def get_config(key: str) -> Any:
return config_manager.get(key)
def set_config(key: str, value: Any) -> bool:
return config_manager.set(key, value)
def reset_config(key: str) -> bool:
return config_manager.reset_to_default(key)