diff --git a/app/core/config.py b/app/core/config.py index 9d5f0c98..b3e28a41 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -82,10 +82,16 @@ class ConfigModel(BaseModel): PORT: int = 3001 # 前端监听端口 NGINX_PORT: int = 3000 + # 配置文件目录 + CONFIG_DIR: Optional[str] = None + # 超级管理员 + SUPERUSER: str = "admin" # 是否调试模式 DEBUG: bool = False # 是否开发模式 DEV: bool = False + # 数据库类型,支持 sqlite 和 postgresql,默认使用 sqlite + DB_TYPE: str = "sqlite" # 是否在控制台输出 SQL 语句,默认关闭 DB_ECHO: bool = False # 数据库连接池类型,QueuePool, NullPool @@ -100,16 +106,26 @@ class ConfigModel(BaseModel): DB_TIMEOUT: int = 60 # SQLite 是否启用 WAL 模式,默认开启 DB_WAL_ENABLE: bool = True + # PostgreSQL 主机地址 + DB_POSTGRESQL_HOST: str = "localhost" + # PostgreSQL 端口 + DB_POSTGRESQL_PORT: int = 5432 + # PostgreSQL 数据库名 + DB_POSTGRESQL_DATABASE: str = "moviepilot" + # PostgreSQL 用户名 + DB_POSTGRESQL_USERNAME: str = "moviepilot" + # PostgreSQL 密码 + DB_POSTGRESQL_PASSWORD: str = "moviepilot" + # PostgreSQL 连接池大小 + DB_POSTGRESQL_POOL_SIZE: int = 20 + # PostgreSQL 连接池溢出数量 + DB_POSTGRESQL_MAX_OVERFLOW: int = 30 # 缓存类型,支持 cachetools 和 redis,默认使用 cachetools CACHE_BACKEND_TYPE: str = "cachetools" # 缓存连接字符串,仅外部缓存(如 Redis、Memcached)需要 CACHE_BACKEND_URL: Optional[str] = None # Redis 缓存最大内存限制,未配置时,如开启大内存模式时为 "1024mb",未开启时为 "256mb" CACHE_REDIS_MAXMEMORY: Optional[str] = None - # 配置文件目录 - CONFIG_DIR: Optional[str] = None - # 超级管理员 - SUPERUSER: str = "admin" # 辅助认证,允许通过外部服务进行认证、单点登录以及自动创建用户 AUXILIARY_AUTH_ENABLE: bool = False # API密钥,需要更换 @@ -124,6 +140,10 @@ class ConfigModel(BaseModel): SEARCH_SOURCE: str = "themoviedb,douban,bangumi" # 媒体识别来源 themoviedb/douban RECOGNIZE_SOURCE: str = "themoviedb" + # 元数据识别缓存过期时间(小时) + META_CACHE_EXPIRE: int = 0 + # 电视剧动漫的分类genre_ids + ANIME_GENREIDS: List[int] = Field(default=[16]) # 刮削来源 themoviedb/douban SCRAP_SOURCE: str = "themoviedb" # 新增已入库媒体是否跟随TMDB信息变化 @@ -151,10 +171,6 @@ class ConfigModel(BaseModel): U115_APP_ID: str = "100196807" # Alipan AppId ALIPAN_APP_ID: str = "ac1bf04dc9fd4d9aaabb65b4a668d403" - # 元数据识别缓存过期时间(小时) - META_CACHE_EXPIRE: int = 0 - # 电视剧动漫的分类genre_ids - ANIME_GENREIDS: List[int] = Field(default=[16]) # 用户认证站点 AUTH_SITE: str = "" # 重启自动升级 diff --git a/app/db/__init__.py b/app/db/__init__.py index 6b7905c4..df056199 100644 --- a/app/db/__init__.py +++ b/app/db/__init__.py @@ -1,19 +1,43 @@ import asyncio -from typing import Any, Generator, List, Optional, Self, Tuple, AsyncGenerator, Sequence, Union +from typing import Any, Generator, List, Optional, Self, Tuple, AsyncGenerator, Union -from sqlalchemy import NullPool, QueuePool, and_, create_engine, inspect, text, select, delete +from sqlalchemy import NullPool, QueuePool, and_, create_engine, inspect, text, select, delete, Column, Integer, \ + Sequence from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from sqlalchemy.orm import Session, as_declarative, declared_attr, scoped_session, sessionmaker from app.core.config import settings +def get_id_column(): + """ + 根据数据库类型返回合适的ID列定义 + """ + if settings.DB_TYPE.lower() == "postgresql": + # PostgreSQL使用SERIAL类型 + return Column(Integer, primary_key=True, index=True) + else: + # SQLite使用Sequence + return Column(Integer, Sequence('id'), primary_key=True, index=True) + + def _get_database_engine(is_async: bool = False): """ 获取数据库连接参数并设置WAL模式 :param is_async: 是否创建异步引擎,True - 异步引擎, False - 同步引擎 :return: 返回对应的数据库引擎 """ + # 根据数据库类型选择连接方式 + if settings.DB_TYPE.lower() == "postgresql": + return _get_postgresql_engine(is_async) + else: + return _get_sqlite_engine(is_async) + + +def _get_sqlite_engine(is_async: bool = False): + """ + 获取SQLite数据库引擎 + """ # 连接参数 _connect_args = { "timeout": settings.DB_TIMEOUT, @@ -52,7 +76,7 @@ def _get_database_engine(is_async: bool = False): _journal_mode = "WAL" if settings.DB_WAL_ENABLE else "DELETE" with engine.connect() as connection: current_mode = connection.execute(text(f"PRAGMA journal_mode={_journal_mode};")).scalar() - print(f"Database journal mode set to: {current_mode}") + print(f"SQLite database journal mode set to: {current_mode}") return engine else: @@ -78,12 +102,68 @@ def _get_database_engine(is_async: bool = False): async with async_engine.connect() as _connection: result = await _connection.execute(text(f"PRAGMA journal_mode={_journal_mode};")) _current_mode = result.scalar() - print(f"Async database journal mode set to: {_current_mode}") + print(f"Async SQLite database journal mode set to: {_current_mode}") try: asyncio.run(set_async_wal_mode()) except Exception as e: - print(f"Failed to set async WAL mode: {e}") + print(f"Failed to set async SQLite WAL mode: {e}") + + return async_engine + + +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}" + + # 创建同步引擎 + if not is_async: + # 根据池类型设置 poolclass 和相关参数 + _pool_class = NullPool if settings.DB_POOL_TYPE == "NullPool" else QueuePool + + # 数据库参数 + _db_kwargs = { + "url": db_url, + "pool_pre_ping": settings.DB_POOL_PRE_PING, + "echo": settings.DB_ECHO, + "poolclass": _pool_class, + "pool_recycle": settings.DB_POOL_RECYCLE, + } + + # 当使用 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 + }) + + # 创建数据库引擎 + engine = create_engine(**_db_kwargs) + print(f"PostgreSQL database connected to {settings.DB_POSTGRESQL_HOST}:{settings.DB_POSTGRESQL_PORT}/{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}" + + # 数据库参数,只能使用 NullPool + _db_kwargs = { + "url": async_db_url, + "pool_pre_ping": settings.DB_POOL_PRE_PING, + "echo": settings.DB_ECHO, + "poolclass": NullPool, + "pool_recycle": settings.DB_POOL_RECYCLE, + } + # 创建异步数据库引擎 + 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}") return async_engine diff --git a/app/db/init.py b/app/db/init.py index d4f838a6..46db4c11 100644 --- a/app/db/init.py +++ b/app/db/init.py @@ -18,12 +18,22 @@ def update_db(): """ 更新数据库 """ - db_location = settings.CONFIG_PATH / 'user.db' script_location = settings.ROOT_PATH / 'database' try: alembic_cfg = Config() alembic_cfg.set_main_option('script_location', str(script_location)) - alembic_cfg.set_main_option('sqlalchemy.url', f"sqlite:///{db_location}") + + # 根据数据库类型设置不同的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}" + else: + db_location = settings.CONFIG_PATH / 'user.db' + db_url = f"sqlite:///{db_location}" + + alembic_cfg.set_main_option('sqlalchemy.url', db_url) upgrade(alembic_cfg, 'head') except Exception as e: logger.error(f'数据库更新失败:{str(e)}') diff --git a/app/db/models/downloadhistory.py b/app/db/models/downloadhistory.py index 8bdeb40a..11d00294 100644 --- a/app/db/models/downloadhistory.py +++ b/app/db/models/downloadhistory.py @@ -1,18 +1,18 @@ import time from typing import Optional -from sqlalchemy import Column, Integer, String, Sequence, JSON, select +from sqlalchemy import Column, Integer, String, JSON, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Session -from app.db import db_query, db_update, Base, async_db_query +from app.db import db_query, db_update, get_id_column, Base, async_db_query class DownloadHistory(Base): """ 下载历史记录 """ - id = Column(Integer, Sequence('id'), primary_key=True, index=True) + id = get_id_column() # 保存路径 path = Column(String, nullable=False, index=True) # 类型 电影/电视剧 @@ -188,7 +188,7 @@ class DownloadFiles(Base): """ 下载文件记录 """ - id = Column(Integer, Sequence('id'), primary_key=True, index=True) + id = get_id_column() # 下载器 downloader = Column(String) # 下载任务Hash diff --git a/app/db/models/mediaserver.py b/app/db/models/mediaserver.py index a9db5732..c15b5d05 100644 --- a/app/db/models/mediaserver.py +++ b/app/db/models/mediaserver.py @@ -1,19 +1,19 @@ from datetime import datetime from typing import Optional -from sqlalchemy import Column, Integer, String, Sequence, JSON +from sqlalchemy import Column, Integer, String, JSON from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Session -from app.db import db_query, db_update, async_db_query, Base +from app.db import db_query, db_update, get_id_column, async_db_query, Base class MediaServerItem(Base): """ 媒体服务器媒体条目表 """ - id = Column(Integer, Sequence('id'), primary_key=True, index=True) + id = get_id_column() # 服务器类型 server = Column(String) # 媒体库ID diff --git a/app/db/models/message.py b/app/db/models/message.py index 33678672..6fcad3de 100644 --- a/app/db/models/message.py +++ b/app/db/models/message.py @@ -1,17 +1,17 @@ from typing import Optional -from sqlalchemy import Column, Integer, String, Sequence, JSON, select +from sqlalchemy import Column, Integer, String, JSON, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Session -from app.db import db_query, Base, async_db_query +from app.db import db_query, Base, get_id_column, async_db_query class Message(Base): """ 消息表 """ - id = Column(Integer, Sequence('id'), primary_key=True, index=True) + id = get_id_column() # 消息渠道 channel = Column(String) # 消息来源 diff --git a/app/db/models/plugindata.py b/app/db/models/plugindata.py index 76200148..3e8e46dd 100644 --- a/app/db/models/plugindata.py +++ b/app/db/models/plugindata.py @@ -1,14 +1,14 @@ -from sqlalchemy import Column, Integer, String, Sequence, JSON +from sqlalchemy import Column, String, JSON from sqlalchemy.orm import Session -from app.db import db_query, db_update, Base +from app.db import db_query, db_update, get_id_column, Base class PluginData(Base): """ 插件数据表 """ - id = Column(Integer, Sequence('id'), primary_key=True, index=True) + id = get_id_column() plugin_id = Column(String, nullable=False, index=True) key = Column(String, index=True, nullable=False) value = Column(JSON) diff --git a/app/db/models/site.py b/app/db/models/site.py index d6d856a1..26946eae 100644 --- a/app/db/models/site.py +++ b/app/db/models/site.py @@ -1,17 +1,17 @@ from datetime import datetime -from sqlalchemy import Boolean, Column, Integer, String, Sequence, JSON, select, delete +from sqlalchemy import Boolean, Column, Integer, String, JSON, select, delete from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Session -from app.db import db_query, db_update, Base, async_db_query, async_db_update +from app.db import db_query, db_update, Base, async_db_query, async_db_update, get_id_column class Site(Base): """ 站点表 """ - id = Column(Integer, Sequence('id'), primary_key=True, index=True) + id = get_id_column() # 站点名 name = Column(String, nullable=False) # 域名Key diff --git a/app/db/models/siteicon.py b/app/db/models/siteicon.py index 14212ae0..05f7593d 100644 --- a/app/db/models/siteicon.py +++ b/app/db/models/siteicon.py @@ -1,15 +1,15 @@ -from sqlalchemy import Column, Integer, String, Sequence, select +from sqlalchemy import Column, String, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Session -from app.db import db_query, Base, async_db_query +from app.db import db_query, Base, get_id_column, async_db_query class SiteIcon(Base): """ 站点图标表 """ - id = Column(Integer, Sequence('id'), primary_key=True, index=True) + id = get_id_column() # 站点名称 name = Column(String, nullable=False) # 域名Key diff --git a/app/db/models/sitestatistic.py b/app/db/models/sitestatistic.py index 77c3f0f9..f2874cff 100644 --- a/app/db/models/sitestatistic.py +++ b/app/db/models/sitestatistic.py @@ -1,17 +1,17 @@ from datetime import datetime -from sqlalchemy import Column, Integer, String, Sequence, JSON, select +from sqlalchemy import Column, Integer, String, JSON, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Session -from app.db import db_query, db_update, Base, async_db_query +from app.db import db_query, db_update, get_id_column, Base, async_db_query class SiteStatistic(Base): """ 站点统计表 """ - id = Column(Integer, Sequence('id'), primary_key=True, index=True) + id = get_id_column() # 域名Key domain = Column(String, index=True) # 成功次数 diff --git a/app/db/models/siteuserdata.py b/app/db/models/siteuserdata.py index a18d356a..17b6002c 100644 --- a/app/db/models/siteuserdata.py +++ b/app/db/models/siteuserdata.py @@ -1,18 +1,18 @@ from datetime import datetime from typing import Optional -from sqlalchemy import Column, Integer, String, Sequence, Float, JSON, func, or_, select +from sqlalchemy import Column, Integer, String, Float, JSON, func, or_, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Session -from app.db import db_query, Base, async_db_query +from app.db import db_query, Base, get_id_column, async_db_query class SiteUserData(Base): """ 站点数据表 """ - id = Column(Integer, Sequence('id'), primary_key=True, index=True) + id = get_id_column() # 站点域名 domain = Column(String, index=True) # 站点名称 diff --git a/app/db/models/subscribe.py b/app/db/models/subscribe.py index e70d1524..a3dd2d44 100644 --- a/app/db/models/subscribe.py +++ b/app/db/models/subscribe.py @@ -1,18 +1,18 @@ import time from typing import Optional -from sqlalchemy import Column, Integer, String, Sequence, Float, JSON, select +from sqlalchemy import Column, Integer, String, Float, JSON, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Session -from app.db import db_query, db_update, Base, async_db_query, async_db_update +from app.db import db_query, db_update, get_id_column, Base, async_db_query, async_db_update class Subscribe(Base): """ 订阅表 """ - id = Column(Integer, Sequence('id'), primary_key=True, index=True) + id = get_id_column() # 标题 name = Column(String, nullable=False, index=True) # 年份 diff --git a/app/db/models/subscribehistory.py b/app/db/models/subscribehistory.py index a1cef575..37d1ca66 100644 --- a/app/db/models/subscribehistory.py +++ b/app/db/models/subscribehistory.py @@ -1,17 +1,17 @@ from typing import Optional -from sqlalchemy import Column, Integer, String, Sequence, Float, JSON, select +from sqlalchemy import Column, Integer, String, Float, JSON, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Session -from app.db import db_query, Base, async_db_query +from app.db import db_query, Base, get_id_column, async_db_query class SubscribeHistory(Base): """ 订阅历史表 """ - id = Column(Integer, Sequence('id'), primary_key=True, index=True) + id = get_id_column() # 标题 name = Column(String, nullable=False, index=True) # 年份 diff --git a/app/db/models/systemconfig.py b/app/db/models/systemconfig.py index 5ecaa039..c7b3dac2 100644 --- a/app/db/models/systemconfig.py +++ b/app/db/models/systemconfig.py @@ -1,15 +1,15 @@ -from sqlalchemy import Column, Integer, String, Sequence, JSON, select +from sqlalchemy import Column, String, JSON, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Session -from app.db import db_query, db_update, Base, async_db_query +from app.db import db_query, db_update, Base, async_db_query, get_id_column class SystemConfig(Base): """ 配置表 """ - id = Column(Integer, Sequence('id'), primary_key=True, index=True) + id = get_id_column() # 主键 key = Column(String, index=True) # 值 diff --git a/app/db/models/transferhistory.py b/app/db/models/transferhistory.py index 2c5c5be9..a72695fb 100644 --- a/app/db/models/transferhistory.py +++ b/app/db/models/transferhistory.py @@ -1,18 +1,18 @@ import time from typing import Optional -from sqlalchemy import Column, Integer, String, Sequence, Boolean, func, or_, JSON, select +from sqlalchemy import Column, Integer, String, Boolean, func, or_, JSON, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Session -from app.db import db_query, db_update, Base, async_db_query +from app.db import db_query, db_update, get_id_column, Base, async_db_query class TransferHistory(Base): """ 整理记录 """ - id = Column(Integer, Sequence('id'), primary_key=True, index=True) + id = get_id_column() # 源路径 src = Column(String, index=True) # 源存储 diff --git a/app/db/models/user.py b/app/db/models/user.py index 09b01161..91f37f9c 100644 --- a/app/db/models/user.py +++ b/app/db/models/user.py @@ -1,8 +1,8 @@ -from sqlalchemy import Boolean, Column, Integer, JSON, Sequence, String, select +from sqlalchemy import Boolean, Column, JSON, String, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Session -from app.db import Base, db_query, db_update, async_db_query, async_db_update +from app.db import Base, db_query, db_update, async_db_query, async_db_update, get_id_column class User(Base): @@ -10,7 +10,7 @@ class User(Base): 用户表 """ # ID - id = Column(Integer, Sequence('id'), primary_key=True, index=True) + id = get_id_column() # 用户名,唯一值 name = Column(String, index=True, nullable=False) # 邮箱 diff --git a/app/db/models/userconfig.py b/app/db/models/userconfig.py index 1570a8f5..b8b6eedf 100644 --- a/app/db/models/userconfig.py +++ b/app/db/models/userconfig.py @@ -1,14 +1,14 @@ -from sqlalchemy import Column, Integer, String, Sequence, UniqueConstraint, Index, JSON +from sqlalchemy import Column, String, UniqueConstraint, Index, JSON from sqlalchemy.orm import Session -from app.db import db_query, db_update, Base +from app.db import db_query, db_update, get_id_column, Base class UserConfig(Base): """ 用户配置表 """ - id = Column(Integer, Sequence('id'), primary_key=True, index=True) + id = get_id_column() # 用户名 username = Column(String, index=True) # 配置键 diff --git a/app/db/models/workflow.py b/app/db/models/workflow.py index af7ef447..69bf4f5f 100644 --- a/app/db/models/workflow.py +++ b/app/db/models/workflow.py @@ -1,10 +1,10 @@ from datetime import datetime from typing import Optional -from sqlalchemy import Column, Integer, JSON, Sequence, String, and_, or_, select +from sqlalchemy import Column, Integer, JSON, String, and_, or_, select from sqlalchemy.ext.asyncio import AsyncSession -from app.db import Base, db_query, db_update, async_db_query, async_db_update +from app.db import Base, db_query, get_id_column, db_update, async_db_query, async_db_update class Workflow(Base): @@ -12,7 +12,7 @@ class Workflow(Base): 工作流表 """ # ID - id = Column(Integer, Sequence('id'), primary_key=True, index=True) + id = get_id_column() # 名称 name = Column(String, index=True, nullable=False) # 描述 diff --git a/config/app.env b/config/app.env index f68ad909..aa484a97 100644 --- a/config/app.env +++ b/config/app.env @@ -9,9 +9,31 @@ SUPERUSER=admin DEV=false # 为指定字幕添加.default后缀设置为默认字幕,支持为'zh-cn','zh-tw','eng'添加默认字幕,未定义或设置为None则不添加 DEFAULT_SUB=zh-cn -# 是否启用内存监控,开启后将定期生成内存快照文件 -MEMORY_ANALYSIS=false -# 内存快照间隔(分钟) -MEMORY_SNAPSHOT_INTERVAL=30 -# 保留的内存快照文件数量 -MEMORY_SNAPSHOT_KEEP_COUNT=20 \ No newline at end of file +# 数据库类型,支持 sqlite 和 postgresql,默认使用 sqlite +DB_TYPE=sqlite +# 是否在控制台输出 SQL 语句,默认关闭 +DB_ECHO=False +# 是否在获取连接时进行预先 ping 操作 +DB_POOL_PRE_PING=True +# 数据库连接的回收时间(秒) +DB_POOL_RECYCLE=300 +# 数据库连接池获取连接的超时时间(秒) +DB_POOL_TIMEOUT=30 +# SQLite 的 busy_timeout 参数,默认为 60 秒 +DB_TIMEOUT=60 +# SQLite 是否启用 WAL 模式,默认开启 +DB_WAL_ENABLE=True +# PostgreSQL 主机地址 +DB_POSTGRESQL_HOST=localhost +# PostgreSQL 端口 +DB_POSTGRESQL_PORT=5432 +# PostgreSQL 数据库名 +DB_POSTGRESQL_DATABASE=moviepilot +# PostgreSQL 用户名 +DB_POSTGRESQL_USERNAME=moviepilot +# PostgreSQL 密码 +DB_POSTGRESQL_PASSWORD=moviepilot +# PostgreSQL 连接池大小 +DB_POSTGRESQL_POOL_SIZE=20 +# PostgreSQL 连接池溢出数量 +DB_POSTGRESQL_MAX_OVERFLOW=30 \ No newline at end of file diff --git a/database/env.py b/database/env.py index 0efb8484..48ce2736 100644 --- a/database/env.py +++ b/database/env.py @@ -40,13 +40,25 @@ def run_migrations_offline() -> None: """ url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, - target_metadata=target_metadata, - literal_binds=True, - dialect_opts={"paramstyle": "named"}, - render_as_batch=True - ) + + # 根据数据库类型配置不同的参数 + if url and "postgresql" in url: + # PostgreSQL配置 + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + else: + # SQLite配置 + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + render_as_batch=True + ) with context.begin_transaction(): context.run_migrations() @@ -66,9 +78,22 @@ def run_migrations_online() -> None: ) with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) + url = config.get_main_option("sqlalchemy.url") + + # 根据数据库类型配置不同的参数 + if url and "postgresql" in url: + # PostgreSQL配置 + context.configure( + connection=connection, + target_metadata=target_metadata + ) + else: + # SQLite配置 + context.configure( + connection=connection, + target_metadata=target_metadata, + render_as_batch=True + ) with context.begin_transaction(): context.run_migrations() diff --git a/docker/Dockerfile b/docker/Dockerfile index a663b995..a6b2046b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -28,6 +28,9 @@ RUN apt-get update -y \ rsync \ ffmpeg \ nano \ + postgresql-client \ + postgresql \ + postgresql-contrib \ && dpkg-reconfigure --frontend noninteractive tzdata \ && \ if [ "$(uname -m)" = "x86_64" ]; \ @@ -84,6 +87,6 @@ RUN cp -f /app/docker/nginx.common.conf /etc/nginx/common.conf \ && curl -sL "https://github.com/jxxghp/MoviePilot-Resources/archive/refs/heads/main.zip" | busybox unzip -d /tmp - \ && mv -f /tmp/MoviePilot-Resources-main/resources.v2/* /app/app/helper/ \ && rm -rf /tmp/* -EXPOSE 3000 +EXPOSE 3000 5432 VOLUME [ "${CONFIG_DIR}" ] ENTRYPOINT [ "/entrypoint.sh" ] diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index b79de987..fb37ac53 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -43,6 +43,16 @@ function load_config_from_app_env() { ["GITHUB_TOKEN"]="" ["MOVIEPILOT_AUTO_UPDATE"]="release" + # database + ["DB_TYPE"]="sqlite" + ["DB_POSTGRESQL_HOST"]="localhost" + ["DB_POSTGRESQL_PORT"]="5432" + ["DB_POSTGRESQL_DATABASE"]="moviepilot" + ["DB_POSTGRESQL_USERNAME"]="moviepilot" + ["DB_POSTGRESQL_PASSWORD"]="moviepilot" + ["DB_POSTGRESQL_POOL_SIZE"]="20" + ["DB_POSTGRESQL_MAX_OVERFLOW"]="30" + # cert ["ENABLE_SSL"]="false" ["SSL_DOMAIN"]="" @@ -211,12 +221,70 @@ chown -R moviepilot:moviepilot \ /var/lib/nginx \ /var/log/nginx chown moviepilot:moviepilot /etc/hosts /tmp + +# 如果使用PostgreSQL,确保数据目录权限正确 +if [ "${DB_TYPE:-sqlite}" = "postgresql" ]; then + POSTGRESQL_DATA_DIR="${CONFIG_DIR}/postgresql" + POSTGRESQL_LOG_DIR="${CONFIG_DIR}/postgresql/logs" + mkdir -p "${POSTGRESQL_DATA_DIR}" "${POSTGRESQL_LOG_DIR}" + chown -R postgres:postgres "${POSTGRESQL_DATA_DIR}" "${POSTGRESQL_LOG_DIR}" +fi # 下载浏览器内核 if [[ "$HTTPS_PROXY" =~ ^https?:// ]] || [[ "$HTTPS_PROXY" =~ ^https?:// ]] || [[ "$PROXY_HOST" =~ ^https?:// ]]; then HTTPS_PROXY="${HTTPS_PROXY:-${https_proxy:-$PROXY_HOST}}" gosu moviepilot:moviepilot playwright install chromium else gosu moviepilot:moviepilot playwright install chromium fi +# 启动PostgreSQL服务 +if [ "${DB_TYPE:-sqlite}" = "postgresql" ]; then + INFO "→ 启动PostgreSQL服务..." + + # 使用配置目录下的postgresql子目录作为数据目录 + POSTGRESQL_DATA_DIR="${CONFIG_DIR}/postgresql" + POSTGRESQL_LOG_DIR="${CONFIG_DIR}/postgresql/logs" + + # 创建PostgreSQL数据目录和日志目录 + mkdir -p "${POSTGRESQL_DATA_DIR}" "${POSTGRESQL_LOG_DIR}" + + # 初始化PostgreSQL数据目录 + if [ ! -f "${POSTGRESQL_DATA_DIR}/postgresql.conf" ]; then + INFO "初始化PostgreSQL数据目录..." + chown -R postgres:postgres "${POSTGRESQL_DATA_DIR}" "${POSTGRESQL_LOG_DIR}" + su - postgres -c "initdb -D '${POSTGRESQL_DATA_DIR}'" + + # 配置PostgreSQL + echo "host all all 0.0.0.0/0 md5" >> "${POSTGRESQL_DATA_DIR}/pg_hba.conf" + echo "listen_addresses = '*'" >> "${POSTGRESQL_DATA_DIR}/postgresql.conf" + echo "port = ${DB_POSTGRESQL_PORT:-5432}" >> "${POSTGRESQL_DATA_DIR}/postgresql.conf" + echo "log_destination = 'stderr'" >> "${POSTGRESQL_DATA_DIR}/postgresql.conf" + echo "logging_collector = on" >> "${POSTGRESQL_DATA_DIR}/postgresql.conf" + echo "log_directory = '${POSTGRESQL_LOG_DIR}'" >> "${POSTGRESQL_DATA_DIR}/postgresql.conf" + echo "log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'" >> "${POSTGRESQL_DATA_DIR}/postgresql.conf" + echo "log_rotation_age = 1d" >> "${POSTGRESQL_DATA_DIR}/postgresql.conf" + echo "log_rotation_size = 100MB" >> "${POSTGRESQL_DATA_DIR}/postgresql.conf" + fi + + # 确保目录权限正确 + chown -R postgres:postgres "${POSTGRESQL_DATA_DIR}" "${POSTGRESQL_LOG_DIR}" + + # 启动PostgreSQL服务 + su - postgres -c "pg_ctl -D '${POSTGRESQL_DATA_DIR}' -l '${POSTGRESQL_LOG_DIR}/postgresql.log' start" + + # 等待PostgreSQL启动 + INFO "等待PostgreSQL服务启动..." + until su - postgres -c "pg_isready -h localhost -p ${DB_POSTGRESQL_PORT:-5432}"; do + sleep 1 + done + + # 创建数据库和用户 + INFO "创建PostgreSQL数据库和用户..." + su - postgres -c "psql -c \"CREATE USER ${DB_POSTGRESQL_USERNAME:-moviepilot} WITH PASSWORD '${DB_POSTGRESQL_PASSWORD:-moviepilot}';\" 2>/dev/null || true" + su - postgres -c "psql -c \"CREATE DATABASE ${DB_POSTGRESQL_DATABASE:-moviepilot} OWNER ${DB_POSTGRESQL_USERNAME:-moviepilot};\" 2>/dev/null || true" + su - postgres -c "psql -c \"GRANT ALL PRIVILEGES ON DATABASE ${DB_POSTGRESQL_DATABASE:-moviepilot} TO ${DB_POSTGRESQL_USERNAME:-moviepilot};\" 2>/dev/null || true" + + INFO "PostgreSQL服务启动完成,数据目录: ${POSTGRESQL_DATA_DIR}" +fi + # 证书管理 source /app/docker/cert.sh # 启动前端nginx服务 diff --git a/requirements.in b/requirements.in index c9a5db0d..8403afce 100644 --- a/requirements.in +++ b/requirements.in @@ -65,6 +65,8 @@ aiofiles~=24.1.0 aiopathlib~=0.6.0 asynctempfile~=0.5.0 aiosqlite~=0.21.0 +psycopg2-binary~=2.9.10 +asyncpg~=0.30.0 jieba~=0.42.1 rsa~=4.9 redis~=6.2.0