diff --git a/app/agent/prompt/__init__.py b/app/agent/prompt/__init__.py index c54771bd..e90089b8 100644 --- a/app/agent/prompt/__init__.py +++ b/app/agent/prompt/__init__.py @@ -281,7 +281,10 @@ class PromptManager: db_info = f"SQLite ({settings.CONFIG_PATH / 'db' / 'moviepilot.db'})" else: db_password = settings.DB_POSTGRESQL_PASSWORD or "" - db_info = f"PostgreSQL ({settings.DB_POSTGRESQL_USERNAME}:{db_password}@{settings.DB_POSTGRESQL_HOST}:{settings.DB_POSTGRESQL_PORT}/{settings.DB_POSTGRESQL_DATABASE})" + db_info = ( + f"PostgreSQL ({settings.DB_POSTGRESQL_USERNAME}:{db_password}@" + f"{settings.DB_POSTGRESQL_TARGET}/{settings.DB_POSTGRESQL_DATABASE})" + ) info_lines = [ f"- 当前时间: {strftime('%Y-%m-%d %H:%M:%S')}", diff --git a/app/core/config.py b/app/core/config.py index 3361fd86..a8a71454 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -10,7 +10,7 @@ import threading from asyncio import AbstractEventLoop from pathlib import Path from typing import Any, Dict, List, Optional, Tuple, Type -from urllib.parse import urlparse +from urllib.parse import quote, urlencode, urlparse from dotenv import set_key from pydantic import BaseModel, Field, ConfigDict, model_validator @@ -126,8 +126,8 @@ class ConfigModel(BaseModel): DB_SQLITE_MAX_OVERFLOW: int = 50 # PostgreSQL 主机地址 DB_POSTGRESQL_HOST: str = "localhost" - # PostgreSQL 端口 - DB_POSTGRESQL_PORT: int = 5432 + # PostgreSQL 端口;使用 Unix Socket 时可留空 + DB_POSTGRESQL_PORT: str = "5432" # PostgreSQL 数据库名 DB_POSTGRESQL_DATABASE: str = "moviepilot" # PostgreSQL 用户名 @@ -142,7 +142,7 @@ class ConfigModel(BaseModel): # ==================== 缓存配置 ==================== # 缓存类型,支持 cachetools 和 redis,默认使用 cachetools CACHE_BACKEND_TYPE: str = "cachetools" - # 缓存连接字符串,仅外部缓存(如 Redis、Memcached)需要 + # 缓存连接字符串,仅外部缓存(如 Redis、Memcached)需要,支持 Redis Unix Socket URL CACHE_BACKEND_URL: Optional[str] = "redis://localhost:6379" # Redis 缓存最大内存限制,未配置时,如开启大内存模式时为 "1024mb",未开启时为 "256mb" CACHE_REDIS_MAXMEMORY: Optional[str] = None @@ -921,6 +921,39 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel): } return None + @property + def DB_POSTGRESQL_SOCKET_MODE(self) -> bool: + host = (self.DB_POSTGRESQL_HOST or "").strip() + return host.startswith("/") + + @property + def DB_POSTGRESQL_TARGET(self) -> str: + if self.DB_POSTGRESQL_SOCKET_MODE: + target = f"socket {self.DB_POSTGRESQL_HOST}" + if self.DB_POSTGRESQL_PORT: + target = f"{target} (port {self.DB_POSTGRESQL_PORT})" + return target + if self.DB_POSTGRESQL_PORT: + return f"{self.DB_POSTGRESQL_HOST}:{self.DB_POSTGRESQL_PORT}" + return self.DB_POSTGRESQL_HOST + + def DB_POSTGRESQL_URL(self, driver: Optional[str] = None) -> str: + scheme = "postgresql" if not driver else f"postgresql+{driver}" + username = quote(str(self.DB_POSTGRESQL_USERNAME), safe="") + database = quote(str(self.DB_POSTGRESQL_DATABASE), safe="") + auth = username + if self.DB_POSTGRESQL_PASSWORD: + auth = f"{auth}:{quote(str(self.DB_POSTGRESQL_PASSWORD), safe='')}" + + if self.DB_POSTGRESQL_SOCKET_MODE: + query = {"host": self.DB_POSTGRESQL_HOST} + if self.DB_POSTGRESQL_PORT: + query["port"] = self.DB_POSTGRESQL_PORT + return f"{scheme}://{auth}@/{database}?{urlencode(query)}" + + port = f":{self.DB_POSTGRESQL_PORT}" if self.DB_POSTGRESQL_PORT else "" + return f"{scheme}://{auth}@{self.DB_POSTGRESQL_HOST}{port}/{database}" + @property def PROXY_SERVER(self): if self.PROXY_HOST: diff --git a/app/db/__init__.py b/app/db/__init__.py index d854a8f0..fc473aa5 100644 --- a/app/db/__init__.py +++ b/app/db/__init__.py @@ -116,11 +116,7 @@ def _get_postgresql_engine(is_async: bool = False): """ 获取PostgreSQL数据库引擎 """ - # 构建PostgreSQL连接URL - if settings.DB_POSTGRESQL_PASSWORD: - db_url = f"postgresql://{settings.DB_POSTGRESQL_USERNAME}:{settings.DB_POSTGRESQL_PASSWORD}@{settings.DB_POSTGRESQL_HOST}:{settings.DB_POSTGRESQL_PORT}/{settings.DB_POSTGRESQL_DATABASE}" - else: - db_url = f"postgresql://{settings.DB_POSTGRESQL_USERNAME}@{settings.DB_POSTGRESQL_HOST}:{settings.DB_POSTGRESQL_PORT}/{settings.DB_POSTGRESQL_DATABASE}" + db_url = settings.DB_POSTGRESQL_URL() # PostgreSQL连接参数 _connect_args = {} @@ -150,12 +146,11 @@ def _get_postgresql_engine(is_async: bool = False): # 创建数据库引擎 engine = create_engine(**_db_kwargs) - print(f"PostgreSQL database connected to {settings.DB_POSTGRESQL_HOST}:{settings.DB_POSTGRESQL_PORT}/{settings.DB_POSTGRESQL_DATABASE}") + print(f"PostgreSQL database connected to {settings.DB_POSTGRESQL_TARGET}/{settings.DB_POSTGRESQL_DATABASE}") return engine else: - # 构建异步PostgreSQL连接URL - async_db_url = f"postgresql+asyncpg://{settings.DB_POSTGRESQL_USERNAME}:{settings.DB_POSTGRESQL_PASSWORD}@{settings.DB_POSTGRESQL_HOST}:{settings.DB_POSTGRESQL_PORT}/{settings.DB_POSTGRESQL_DATABASE}" + async_db_url = settings.DB_POSTGRESQL_URL("asyncpg") # 数据库参数,只能使用 NullPool _db_kwargs = { @@ -168,7 +163,7 @@ def _get_postgresql_engine(is_async: bool = False): } # 创建异步数据库引擎 async_engine = create_async_engine(**_db_kwargs) - print(f"Async PostgreSQL database connected to {settings.DB_POSTGRESQL_HOST}:{settings.DB_POSTGRESQL_PORT}/{settings.DB_POSTGRESQL_DATABASE}") + print(f"Async PostgreSQL database connected to {settings.DB_POSTGRESQL_TARGET}/{settings.DB_POSTGRESQL_DATABASE}") return async_engine diff --git a/app/db/init.py b/app/db/init.py index 66a21733..0515ffb7 100644 --- a/app/db/init.py +++ b/app/db/init.py @@ -28,10 +28,7 @@ def update_db(): # 根据数据库类型设置不同的URL if settings.DB_TYPE.lower() == "postgresql": - if settings.DB_POSTGRESQL_PASSWORD: - db_url = f"postgresql://{settings.DB_POSTGRESQL_USERNAME}:{settings.DB_POSTGRESQL_PASSWORD}@{settings.DB_POSTGRESQL_HOST}:{settings.DB_POSTGRESQL_PORT}/{settings.DB_POSTGRESQL_DATABASE}" - else: - db_url = f"postgresql://{settings.DB_POSTGRESQL_USERNAME}@{settings.DB_POSTGRESQL_HOST}:{settings.DB_POSTGRESQL_PORT}/{settings.DB_POSTGRESQL_DATABASE}" + db_url = settings.DB_POSTGRESQL_URL() else: db_location = settings.CONFIG_PATH / 'user.db' db_url = f"sqlite:///{db_location}" diff --git a/docs/postgresql-setup.md b/docs/postgresql-setup.md index bfc3eaae..9d1bdde5 100644 --- a/docs/postgresql-setup.md +++ b/docs/postgresql-setup.md @@ -24,7 +24,7 @@ DB_TYPE=postgresql # PostgreSQL 主机地址 DB_POSTGRESQL_HOST=localhost -# PostgreSQL 端口 +# PostgreSQL 端口;使用 Unix Socket 时可留空 DB_POSTGRESQL_PORT=5432 # PostgreSQL 数据库名 @@ -43,6 +43,21 @@ DB_POSTGRESQL_POOL_SIZE=20 DB_POSTGRESQL_MAX_OVERFLOW=30 ``` +### 3. Unix Socket 连接 + +如果 PostgreSQL 通过 Unix Socket 暴露,可以把 `DB_POSTGRESQL_HOST` 设置为套接字目录。 + +```bash +DB_TYPE=postgresql +DB_POSTGRESQL_HOST=/var/run/postgresql +DB_POSTGRESQL_PORT= +DB_POSTGRESQL_DATABASE=moviepilot +DB_POSTGRESQL_USERNAME=moviepilot +DB_POSTGRESQL_PASSWORD=moviepilot +``` + +如需显式指定 socket 端口,也可以保留 `DB_POSTGRESQL_PORT`,程序会生成带 `host=/path/to/socket` 查询参数的 PostgreSQL URL。 + ## Docker 部署 ### 使用外部 PostgreSQL @@ -60,6 +75,13 @@ DB_POSTGRESQL_USERNAME=your-username DB_POSTGRESQL_PASSWORD=your-password ``` +使用 Redis Unix Socket 时,可直接设置 `CACHE_BACKEND_URL`,例如: + +```bash +CACHE_BACKEND_TYPE=redis +CACHE_BACKEND_URL=unix:///var/run/redis/redis.sock?db=0 +``` + ## 数据迁移 ### 从 SQLite 迁移到 PostgreSQL diff --git a/tests/test_postgresql_socket_config.py b/tests/test_postgresql_socket_config.py new file mode 100644 index 00000000..ca586bf8 --- /dev/null +++ b/tests/test_postgresql_socket_config.py @@ -0,0 +1,101 @@ +import sys +import unittest +from enum import Enum +from types import ModuleType + + +def _stub_module(name: str, **attrs): + module = sys.modules.get(name) + if module is None: + module = ModuleType(name) + sys.modules[name] = module + for key, value in attrs.items(): + setattr(module, key, value) + return module + + +class _DummyLogger: + def __getattr__(self, _name): + return lambda *args, **kwargs: None + + +_stub_module( + "app.log", + logger=_DummyLogger(), + log_settings=_DummyLogger(), + LogConfigModel=type("LogConfigModel", (), {}), +) +_stub_module("psutil") +_schemas_module = _stub_module( + "app.schemas", MediaType=Enum("MediaType", {"Movie": "Movie", "TV": "TV"}) +) +_schemas_module.__getattr__ = lambda name: type(name, (), {}) +_stub_module("version", APP_VERSION="test") + + +from app.core.config import Settings + + +class PostgreSQLSocketConfigTests(unittest.TestCase): + def test_postgresql_tcp_url_keeps_host_and_port(self): + settings = Settings( + DB_POSTGRESQL_HOST="db", + DB_POSTGRESQL_PORT="5433", + DB_POSTGRESQL_DATABASE="moviepilot", + DB_POSTGRESQL_USERNAME="user", + DB_POSTGRESQL_PASSWORD="pass", + ) + + self.assertFalse(settings.DB_POSTGRESQL_SOCKET_MODE) + self.assertEqual( + settings.DB_POSTGRESQL_URL(), + "postgresql://user:pass@db:5433/moviepilot", + ) + self.assertEqual( + settings.DB_POSTGRESQL_URL("asyncpg"), + "postgresql+asyncpg://user:pass@db:5433/moviepilot", + ) + self.assertEqual(settings.DB_POSTGRESQL_TARGET, "db:5433") + + def test_postgresql_socket_url_uses_host_query_param(self): + settings = Settings( + DB_POSTGRESQL_HOST="/var/run/postgresql", + DB_POSTGRESQL_PORT="", + DB_POSTGRESQL_DATABASE="moviepilot", + DB_POSTGRESQL_USERNAME="user", + DB_POSTGRESQL_PASSWORD="pass", + ) + + self.assertTrue(settings.DB_POSTGRESQL_SOCKET_MODE) + self.assertIsNone(settings.DB_POSTGRESQL_PORT_VALUE) + self.assertEqual( + settings.DB_POSTGRESQL_URL(), + "postgresql://user:pass@/moviepilot?host=%2Fvar%2Frun%2Fpostgresql", + ) + self.assertEqual( + settings.DB_POSTGRESQL_URL("asyncpg"), + "postgresql+asyncpg://user:pass@/moviepilot?host=%2Fvar%2Frun%2Fpostgresql", + ) + self.assertEqual(settings.DB_POSTGRESQL_TARGET, "socket /var/run/postgresql") + + def test_postgresql_socket_url_can_keep_explicit_port(self): + settings = Settings( + DB_POSTGRESQL_HOST="/var/run/postgresql", + DB_POSTGRESQL_PORT="5432", + DB_POSTGRESQL_DATABASE="moviepilot", + DB_POSTGRESQL_USERNAME="user", + DB_POSTGRESQL_PASSWORD="", + ) + + self.assertEqual( + settings.DB_POSTGRESQL_URL(), + "postgresql://user@/moviepilot?host=%2Fvar%2Frun%2Fpostgresql&port=5432", + ) + self.assertEqual( + settings.DB_POSTGRESQL_TARGET, + "socket /var/run/postgresql (port 5432)", + ) + + +if __name__ == "__main__": + unittest.main()