Compare commits

..

31 Commits

Author SHA1 Message Date
jxxghp
db0ea7d6c4 Fix database sequence errors (#4777)
* Fix database upgrade script to handle existing identity columns

Co-authored-by: jxxghp <jxxghp@live.cn>

* Improve identity column conversion with error handling and cleanup

Co-authored-by: jxxghp <jxxghp@live.cn>

* Fix database upgrade script to handle existing identity columns

Co-authored-by: jxxghp <jxxghp@live.cn>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: jxxghp <jxxghp@live.cn>
2025-08-20 00:29:35 +08:00
jxxghp
1eb85003de 更新 version.py 2025-08-19 17:58:27 +08:00
jxxghp
cca170f84a 更新 emby.py 2025-08-19 15:30:22 +08:00
jxxghp
c8c016caa8 更新 __init__.py 2025-08-19 14:27:02 +08:00
jxxghp
45d5874026 更新 __init__.py 2025-08-19 14:20:46 +08:00
jxxghp
69b1ce60ff fix db config 2025-08-19 14:15:33 +08:00
jxxghp
3ff3e4b106 fix db config 2025-08-19 14:05:24 +08:00
jxxghp
dc50a68b01 修复数据库表名引用 2025-08-19 12:54:47 +08:00
jxxghp
968cfd8654 fix db 2025-08-19 12:41:07 +08:00
jxxghp
cf28d93be6 fix db 2025-08-19 12:35:52 +08:00
jxxghp
be08d6ebb5 fix db 2025-08-19 12:02:53 +08:00
jxxghp
4bc24f3b00 fix db 2025-08-19 11:53:59 +08:00
jxxghp
15833f94cf fix db 2025-08-19 11:40:34 +08:00
jxxghp
aeb297efcf 优化站点激活状态的判断逻辑,简化数据库查询条件 2025-08-19 11:23:09 +08:00
jxxghp
d48c6b98e8 rollback local postgresql 2025-08-19 08:30:07 +08:00
jxxghp
b79ccfafed 优化 entrypoint.sh 中 PostgreSQL 命令的执行方式 2025-08-19 07:15:02 +08:00
jxxghp
c87ba59552 更新 entrypoint.sh 2025-08-18 22:42:55 +08:00
jxxghp
91fd71c858 fix entrypoint.sh 2025-08-18 22:26:01 +08:00
jxxghp
6f64e67538 fix dockerfile 2025-08-18 21:42:44 +08:00
jxxghp
bd7a0b072f fix entrypoint.sh 2025-08-18 21:22:29 +08:00
jxxghp
01ca001c97 fix entrypoint.sh 2025-08-18 21:10:24 +08:00
jxxghp
324ad2a87c 优化 PostgreSQL 数据目录初始化和启动逻辑 2025-08-18 20:55:33 +08:00
jxxghp
d9ad2630f0 fix postgresql 2025-08-18 19:14:47 +08:00
jxxghp
83958a4a48 fix postgresql 2025-08-18 19:12:20 +08:00
jxxghp
f6a6efdc42 fix app.env 2025-08-18 15:17:26 +08:00
jxxghp
1bbe7657b9 fix dockerfile 2025-08-18 11:42:53 +08:00
jxxghp
38189753b5 在构建工作流中添加新的 Docker 镜像配置 2025-08-18 11:31:00 +08:00
jxxghp
5b0e658617 重构配置文件项目顺序 2025-08-18 11:29:04 +08:00
jxxghp
b6cf54d57f 添加对 PostgreSQL 的支持 2025-08-18 11:19:17 +08:00
jxxghp
e8058c8813 添加 PostgreSQL 数据库支持 2025-08-18 11:19:06 +08:00
jxxghp
784868048d 更新 scheduler.py 2025-08-18 07:04:39 +08:00
31 changed files with 787 additions and 180 deletions

59
.github/workflows/beta.yml vendored Normal file
View File

@@ -0,0 +1,59 @@
name: MoviePilot Builder Beta
on:
workflow_dispatch:
jobs:
Docker-build:
runs-on: ubuntu-latest
name: Build Docker Image
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Release version
id: release_version
run: |
app_version=$(cat version.py |sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp")
echo "app_version=$app_version" >> $GITHUB_ENV
- name: Docker Meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ secrets.DOCKER_USERNAME }}/moviepilot-v2
ghcr.io/${{ github.repository }}
tags: |
type=raw,value=beta
- name: Set Up QEMU
uses: docker/setup-qemu-action@v3
- name: Set Up Buildx
uses: docker/setup-buildx-action@v3
- name: Login DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Image
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile
platforms: |
linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha, scope=${{ github.workflow }}-docker
cache-to: type=gha, scope=${{ github.workflow }}-docker

View File

@@ -27,6 +27,7 @@ jobs:
with:
images: |
${{ secrets.DOCKER_USERNAME }}/moviepilot-v2
${{ secrets.DOCKER_USERNAME }}/moviepilot
ghcr.io/${{ github.repository }}
tags: |
type=raw,value=${{ env.app_version }}

View File

@@ -317,7 +317,7 @@ class SiteChain(ChainBase):
indexer = siteshelper.get_indexer(domain)
# 数据库的站点信息
site_info = siteoper.get_by_domain(domain)
if site_info and site_info.is_active == 1:
if site_info and site_info.is_active:
# 站点已存在,检查站点连通性
status, msg = self.test(domain)
# 更新站点Cookie

View File

@@ -42,10 +42,6 @@ class SystemConfModel(BaseModel):
scheduler: int = 0
# 线程池大小
threadpool: int = 0
# 数据库连接池大小
dbpool: int = 0
# 数据库连接池溢出数量
dbpooloverflow: int = 0
class ConfigModel(BaseModel):
@@ -56,6 +52,7 @@ class ConfigModel(BaseModel):
class Config:
extra = "ignore" # 忽略未定义的配置项
# ==================== 基础应用配置 ====================
# 项目名称
PROJECT_NAME: str = "MoviePilot"
# 域名 格式https://movie-pilot.org
@@ -64,6 +61,22 @@ class ConfigModel(BaseModel):
API_V1_STR: str = "/api/v1"
# 前端资源路径
FRONTEND_PATH: str = "/public"
# 时区
TZ: str = "Asia/Shanghai"
# API监听地址
HOST: str = "0.0.0.0"
# API监听端口
PORT: int = 3001
# 前端监听端口
NGINX_PORT: int = 3000
# 配置文件目录
CONFIG_DIR: Optional[str] = None
# 是否调试模式
DEBUG: bool = False
# 是否开发模式
DEV: bool = False
# ==================== 安全认证配置 ====================
# 密钥
SECRET_KEY: str = secrets.token_urlsafe(32)
# RESOURCE密钥
@@ -74,20 +87,24 @@ class ConfigModel(BaseModel):
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
# RESOURCE_TOKEN过期时间
RESOURCE_ACCESS_TOKEN_EXPIRE_SECONDS: int = 60 * 30
# 时区
TZ: str = "Asia/Shanghai"
# API监听地址
HOST: str = "0.0.0.0"
# API监听端口
PORT: int = 3001
# 前端监听端口
NGINX_PORT: int = 3000
# 是否调试模式
DEBUG: bool = False
# 是否开发模式
DEV: bool = False
# 超级管理员
SUPERUSER: str = "admin"
# 辅助认证,允许通过外部服务进行认证、单点登录以及自动创建用户
AUXILIARY_AUTH_ENABLE: bool = False
# API密钥,需要更换
API_TOKEN: Optional[str] = None
# 用户认证站点
AUTH_SITE: str = ""
# ==================== 数据库配置 ====================
# 数据库类型,支持 sqlite 和 postgresql默认使用 sqlite
DB_TYPE: str = "sqlite"
# 是否在控制台输出 SQL 语句,默认关闭
DB_ECHO: bool = False
# 数据库连接超时时间(秒),默认为 60 秒
DB_TIMEOUT: int = 60
# 是否启用 WAL 模式仅适用于SQLite默认开启
DB_WAL_ENABLE: bool = True
# 数据库连接池类型QueuePool, NullPool
DB_POOL_TYPE: str = "QueuePool"
# 是否在获取连接时进行预先 ping 操作
@@ -96,71 +113,36 @@ class ConfigModel(BaseModel):
DB_POOL_RECYCLE: int = 300
# 数据库连接池获取连接的超时时间(秒)
DB_POOL_TIMEOUT: int = 30
# SQLite 的 busy_timeout 参数,默认为 60 秒
DB_TIMEOUT: int = 60
# SQLite 是否启用 WAL 模式,默认开启
DB_WAL_ENABLE: bool = True
# SQLite 连接池大小
DB_SQLITE_POOL_SIZE: int = 30
# SQLite 连接池溢出数量
DB_SQLITE_MAX_OVERFLOW: int = 50
# 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 = 30
# PostgreSQL 连接池溢出数量
DB_POSTGRESQL_MAX_OVERFLOW: int = 50
# ==================== 缓存配置 ====================
# 缓存类型,支持 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密钥需要更换
API_TOKEN: Optional[str] = None
# ==================== 网络代理配置 ====================
# 网络代理服务器地址
PROXY_HOST: Optional[str] = None
# 登录页面电影海报,tmdb/bing/mediaserver
WALLPAPER: str = "tmdb"
# 自定义壁纸api地址
CUSTOMIZE_WALLPAPER_API_URL: Optional[str] = None
# 媒体搜索来源 themoviedb/douban/bangumi多个用,分隔
SEARCH_SOURCE: str = "themoviedb,douban,bangumi"
# 媒体识别来源 themoviedb/douban
RECOGNIZE_SOURCE: str = "themoviedb"
# 刮削来源 themoviedb/douban
SCRAP_SOURCE: str = "themoviedb"
# 新增已入库媒体是否跟随TMDB信息变化
SCRAP_FOLLOW_TMDB: bool = True
# TMDB图片地址
TMDB_IMAGE_DOMAIN: str = "image.tmdb.org"
# TMDB API地址
TMDB_API_DOMAIN: str = "api.themoviedb.org"
# TMDB元数据语言
TMDB_LOCALE: str = "zh"
# 刮削使用TMDB原始语种图片
TMDB_SCRAP_ORIGINAL_IMAGE: bool = False
# TMDB API Key
TMDB_API_KEY: str = "db55323b8d3e4154498498a75642b381"
# TVDB API Key
TVDB_V4_API_KEY: str = "ed2aa66b-7899-4677-92a7-67bc9ce3d93a"
TVDB_V4_API_PIN: str = ""
# Fanart开关
FANART_ENABLE: bool = True
# Fanart语言
FANART_LANG: str = "zh,en"
# Fanart API Key
FANART_API_KEY: str = "d2d31f9ecabea050fc7d68aa3146015f"
# 115 AppId
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 = ""
# 重启自动升级
MOVIEPILOT_AUTO_UPDATE: str = 'release'
# 自动检查和更新站点资源包(站点索引、认证等)
AUTO_UPDATE_RESOURCE: bool = True
# 是否启用DOH解析域名
DOH_ENABLE: bool = False
# 使用 DOH 解析的域名列表
@@ -174,6 +156,65 @@ class ConfigModel(BaseModel):
"api.telegram.org")
# DOH 解析服务器列表
DOH_RESOLVERS: str = "1.0.0.1,1.1.1.1,9.9.9.9,149.112.112.112"
# ==================== 媒体元数据配置 ====================
# 媒体搜索来源 themoviedb/douban/bangumi多个用,分隔
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信息变化
SCRAP_FOLLOW_TMDB: bool = True
# 检查本地媒体库是否存在资源开关
LOCAL_EXISTS_SEARCH: bool = False
# 搜索多个名称
SEARCH_MULTIPLE_NAME: bool = False
# 最大搜索名称数量
MAX_SEARCH_NAME_LIMIT: int = 2
# ==================== TMDB配置 ====================
# TMDB图片地址
TMDB_IMAGE_DOMAIN: str = "image.tmdb.org"
# TMDB API地址
TMDB_API_DOMAIN: str = "api.themoviedb.org"
# TMDB元数据语言
TMDB_LOCALE: str = "zh"
# 刮削使用TMDB原始语种图片
TMDB_SCRAP_ORIGINAL_IMAGE: bool = False
# TMDB API Key
TMDB_API_KEY: str = "db55323b8d3e4154498498a75642b381"
# ==================== TVDB配置 ====================
# TVDB API Key
TVDB_V4_API_KEY: str = "ed2aa66b-7899-4677-92a7-67bc9ce3d93a"
TVDB_V4_API_PIN: str = ""
# ==================== Fanart配置 ====================
# Fanart开关
FANART_ENABLE: bool = True
# Fanart语言
FANART_LANG: str = "zh,en"
# Fanart API Key
FANART_API_KEY: str = "d2d31f9ecabea050fc7d68aa3146015f"
# ==================== 云盘配置 ====================
# 115 AppId
U115_APP_ID: str = "100196807"
# Alipan AppId
ALIPAN_APP_ID: str = "ac1bf04dc9fd4d9aaabb65b4a668d403"
# ==================== 系统升级配置 ====================
# 重启自动升级
MOVIEPILOT_AUTO_UPDATE: str = 'release'
# 自动检查和更新站点资源包(站点索引、认证等)
AUTO_UPDATE_RESOURCE: bool = True
# ==================== 媒体文件格式配置 ====================
# 支持的后缀格式
RMT_MEDIAEXT: list = Field(
default_factory=lambda: ['.mp4', '.mkv', '.ts', '.iso',
@@ -198,8 +239,12 @@ class ConfigModel(BaseModel):
)
# 下载器临时文件后缀
DOWNLOAD_TMPEXT: list = Field(default_factory=lambda: ['.!qb', '.part'])
# ==================== 媒体服务器配置 ====================
# 媒体服务器同步间隔(小时)
MEDIASERVER_SYNC_INTERVAL: int = 6
# ==================== 订阅配置 ====================
# 订阅模式
SUBSCRIBE_MODE: str = "spider"
# RSS订阅模式刷新时间间隔分钟
@@ -208,24 +253,24 @@ class ConfigModel(BaseModel):
SUBSCRIBE_STATISTIC_SHARE: bool = True
# 订阅搜索开关
SUBSCRIBE_SEARCH: bool = False
# 检查本地媒体库是否存在资源开关
LOCAL_EXISTS_SEARCH: bool = False
# 搜索多个名称
SEARCH_MULTIPLE_NAME: bool = False
# 最大搜索名称数量
MAX_SEARCH_NAME_LIMIT: int = 2
# ==================== 站点配置 ====================
# 站点数据刷新间隔(小时)
SITEDATA_REFRESH_INTERVAL: int = 6
# 读取和发送站点消息
SITE_MESSAGE: bool = True
# 不能缓存站点资源的站点域名,多个使用,分隔
NO_CACHE_SITE_KEY: str = "m-team"
# ==================== 下载配置 ====================
# 种子标签
TORRENT_TAG: str = "MOVIEPILOT"
# 下载站点字幕
DOWNLOAD_SUBTITLE: bool = True
# 交互搜索自动下载用户ID使用,分割
AUTO_DOWNLOAD_USER: Optional[str] = None
# ==================== CookieCloud配置 ====================
# CookieCloud是否启动本地服务
COOKIECLOUD_ENABLE_LOCAL: Optional[bool] = False
# CookieCloud服务器地址
@@ -238,6 +283,8 @@ class ConfigModel(BaseModel):
COOKIECLOUD_INTERVAL: Optional[int] = 60 * 24
# CookieCloud同步黑名单多个域名,分割
COOKIECLOUD_BLACKLIST: Optional[str] = None
# ==================== 重命名配置 ====================
# 电影重命名格式
MOVIE_RENAME_FORMAT: str = "{{title}}{% if year %} ({{year}}){% endif %}" \
"/{{title}}{% if year %} ({{year}}){% endif %}{% if part %}-{{part}}{% endif %}{% if videoFormat %} - {{videoFormat}}{% endif %}" \
@@ -247,10 +294,22 @@ class ConfigModel(BaseModel):
"/Season {{season}}" \
"/{{title}} - {{season_episode}}{% if part %}-{{part}}{% endif %}{% if episode %} - 第 {{episode}} 集{% endif %}" \
"{{fileExt}}"
# 重命名时支持的S0别名
RENAME_FORMAT_S0_NAMES: list = Field(default=["Specials", "SPs"])
# 为指定默认字幕添加.default后缀
DEFAULT_SUB: Optional[str] = "zh-cn"
# ==================== 服务地址配置 ====================
# OCR服务器地址
OCR_HOST: str = "https://movie-pilot.org"
# 服务器地址,对应 https://github.com/jxxghp/MoviePilot-Server 项目
MP_SERVER_HOST: str = "https://movie-pilot.org"
# 登录页面电影海报,tmdb/bing/mediaserver
WALLPAPER: str = "tmdb"
# 自定义壁纸api地址
CUSTOMIZE_WALLPAPER_API_URL: Optional[str] = None
# ==================== 插件配置 ====================
# 插件市场仓库地址,多个地址使用,分隔,地址以/结尾
PLUGIN_MARKET: str = ("https://github.com/jxxghp/MoviePilot-Plugins,"
"https://github.com/thsrite/MoviePilot-Plugins,"
@@ -271,6 +330,8 @@ class ConfigModel(BaseModel):
PLUGIN_STATISTIC_SHARE: bool = True
# 是否开启插件热加载
PLUGIN_AUTO_RELOAD: bool = False
# ==================== GitHub配置 ====================
# Github token提高请求api限流阈值 ghp_****
GITHUB_TOKEN: Optional[str] = None
# Github代理服务器格式https://mirror.ghproxy.com/
@@ -279,6 +340,8 @@ class ConfigModel(BaseModel):
PIP_PROXY: Optional[str] = ''
# 指定的仓库Github token多个仓库使用,分隔,格式:{user1}/{repo1}:ghp_****,{user2}/{repo2}:github_pat_****
REPO_GITHUB_TOKEN: Optional[str] = None
# ==================== 性能配置 ====================
# 大内存模式
BIG_MEMORY_MODE: bool = False
# FastApi性能监控
@@ -289,6 +352,8 @@ class ConfigModel(BaseModel):
ENCODING_DETECTION_PERFORMANCE_MODE: bool = True
# 编码探测的最低置信度阈值
ENCODING_DETECTION_MIN_CONFIDENCE: float = 0.8
# ==================== 安全配置 ====================
# 允许的图片缓存域名
SECURITY_IMAGE_DOMAINS: list = Field(default=[
"image.tmdb.org",
@@ -308,23 +373,27 @@ class ConfigModel(BaseModel):
])
# 允许的图片文件后缀格式
SECURITY_IMAGE_SUFFIXES: list = Field(default=[".jpg", ".jpeg", ".png", ".webp", ".gif", ".svg", ".avif"])
# 重命名时支持的S0别名
RENAME_FORMAT_S0_NAMES: list = Field(default=["Specials", "SPs"])
# 为指定默认字幕添加.default后缀
DEFAULT_SUB: Optional[str] = "zh-cn"
# Docker Client API地址
DOCKER_CLIENT_API: Optional[str] = "tcp://127.0.0.1:38379"
# ==================== 工作流配置 ====================
# 工作流数据共享
WORKFLOW_STATISTIC_SHARE: bool = True
# ==================== 存储配置 ====================
# 对rclone进行快照对比时是否检查文件夹的修改时间
RCLONE_SNAPSHOT_CHECK_FOLDER_MODTIME = True
# 对OpenList进行快照对比时是否检查文件夹的修改时间
OPENLIST_SNAPSHOT_CHECK_FOLDER_MODTIME = True
# ==================== 浏览器仿真配置 ====================
# 仿真类型playwright 或 flaresolverr
BROWSER_EMULATION: str = "playwright"
# FlareSolverr 服务地址,例如 http://127.0.0.1:8191
FLARESOLVERR_URL: Optional[str] = None
# ==================== Docker配置 ====================
# Docker Client API地址
DOCKER_CLIENT_API: Optional[str] = "tcp://127.0.0.1:38379"
class Settings(BaseSettings, ConfigModel, LogConfigModel):
"""
@@ -590,9 +659,7 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
fanart=512,
meta=(self.META_CACHE_EXPIRE or 24) * 3600,
scheduler=100,
threadpool=100,
dbpool=100,
dbpooloverflow=50
threadpool=100
)
return SystemConfModel(
torrents=100,
@@ -603,9 +670,7 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
fanart=128,
meta=(self.META_CACHE_EXPIRE or 2) * 3600,
scheduler=50,
threadpool=50,
dbpool=50,
dbpooloverflow=20
threadpool=50
)
@property

View File

@@ -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, Identity
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, Identity(start=1, cycle=True), 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,
@@ -40,9 +64,9 @@ def _get_database_engine(is_async: bool = False):
# 当使用 QueuePool 时,添加 QueuePool 特有的参数
if _pool_class == QueuePool:
_db_kwargs.update({
"pool_size": settings.CONF.dbpool,
"pool_size": settings.DB_SQLITE_POOL_SIZE,
"pool_timeout": settings.DB_POOL_TIMEOUT,
"max_overflow": settings.CONF.dbpooloverflow
"max_overflow": settings.DB_SQLITE_MAX_OVERFLOW
})
# 创建数据库引擎
@@ -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,73 @@ 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}"
# PostgreSQL连接参数
_connect_args = {}
# 创建同步引擎
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,
"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
})
# 创建数据库引擎
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,
"connect_args": _connect_args
}
# 创建异步数据库引擎
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

View File

@@ -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)}')

View File

@@ -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

View File

@@ -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

View File

@@ -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)
# 消息来源

View File

@@ -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)

View File

@@ -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
@@ -69,12 +69,12 @@ class Site(Base):
@classmethod
@db_query
def get_actives(cls, db: Session):
return db.query(cls).filter(cls.is_active == 1).all()
return db.query(cls).filter(cls.is_active).all()
@classmethod
@async_db_query
async def async_get_actives(cls, db: AsyncSession):
result = await db.execute(select(cls).where(cls.is_active == 1))
result = await db.execute(select(cls).where(cls.is_active))
return result.scalars().all()
@classmethod

View File

@@ -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

View File

@@ -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)
# 成功次数

View File

@@ -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)
# 站点名称

View File

@@ -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)
# 年份

View File

@@ -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)
# 年份

View File

@@ -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)
# 值

View File

@@ -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)
# 源存储

View File

@@ -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)
# 邮箱

View File

@@ -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)
# 配置键

View File

@@ -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)
# 描述

View File

@@ -497,7 +497,7 @@ class Emby:
logger.info(f"影片图片链接:{res.url}")
return res.url
else:
logger.error("Items/Id/Images 未获取到返回数据或无该影片{}图片".format(image_type))
logger.info("Items/Id/Images 未获取到返回数据或无该影片{}图片".format(image_type))
return None
except Exception as e:
logger.error(f"连接Items/Id/Images出错" + str(e))

View File

@@ -390,7 +390,7 @@ class Scheduler(metaclass=Singleton):
if not job:
return None
if job.get("running"):
logger.warning(f"定时任务 {job_id} - {job.get("name")} 正在运行 ...")
logger.warning(f"定时任务 {job_id} - {job.get('name')} 正在运行 ...")
return None
self._jobs[job_id]["running"] = True
return job

View File

@@ -1,17 +1 @@
#######################################################################################################
# V2版本中大部分设置可通过后台设置界面进行配置本文件仅展示界面无法配置的项 这些项同样可以通过环境变量进行设置 #
#######################################################################################################
# 【*】API监听地址注意不是前端访问地址
HOST=0.0.0.0
# 【*】超级管理员,设置后一但重启将固化到数据库中,修改将无效(初始化超级管理员密码仅会生成一次,请在日志中查看并自行登录系统修改)
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
# MoviePilot V2版本大部分设置可通过后台设置界面进行配置仅个别配置需要通过环境变量或本配置文件配置所有可配置项参考https://wiki.movie-pilot.org/zh/configuration

View File

@@ -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()

View File

@@ -0,0 +1,117 @@
"""2.2.0
Revision ID: 5b3355c964bb
Revises: d58298a0879f
Create Date: 2025-08-19 12:27:08.451371
"""
import sqlalchemy as sa
from alembic import op
from app.core.config import settings
# revision identifiers, used by Alembic.
revision = '5b3355c964bb'
down_revision = 'd58298a0879f'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
if settings.DB_TYPE.lower() == "postgresql":
# 将SQLite的Sequence转换为PostgreSQL的Identity
fix_postgresql_sequences()
# ### end Alembic commands ###
def fix_postgresql_sequences():
"""
修复PostgreSQL数据库中的序列问题
将SQLite迁移过来的Sequence转换为PostgreSQL的Identity
"""
connection = op.get_bind()
# 获取所有表名
result = connection.execute(sa.text("""
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
"""))
tables = [row[0] for row in result.fetchall()]
print(f"发现 {len(tables)} 个表需要检查序列")
for table_name in tables:
fix_table_sequence(connection, table_name)
def fix_table_sequence(connection, table_name):
"""
修复单个表的序列
"""
try:
# 跳过alembic_version表它没有id列
if table_name == 'alembic_version':
print(f"跳过表 {table_name}这是Alembic版本表")
return
# 检查表是否有id列
result = connection.execute(sa.text(f"""
SELECT is_identity, column_default
FROM information_schema.columns
WHERE table_name = '{table_name}'
AND column_name = 'id'
"""))
id_column = result.fetchone()
if not id_column:
print(f"{table_name} 没有id列跳过")
return
is_identity, column_default = id_column
# 检查是否已经是Identity类型
if is_identity == 'YES' or (column_default and 'GENERATED BY DEFAULT AS IDENTITY' in column_default):
print(f"{table_name} 的id列已经是Identity类型跳过")
return
# 检查是否有序列
print(f"{table_name} 存在序列,需要修复")
convert_to_identity(connection, table_name)
except Exception as e:
print(f"修复表 {table_name} 序列时出错: {e}")
# 回滚当前事务,避免影响后续操作
connection.rollback()
def convert_to_identity(connection, table_name):
"""
将序列转换为Identity保持原有约束不变
"""
try:
# 获取当前序列的最大值
result = connection.execute(sa.text(f"""
SELECT COALESCE(MAX(id), 0) + 1 as next_value
FROM "{table_name}"
"""))
next_value = result.fetchone()[0]
# 直接修改列属性添加Identity保持其他约束不变
# 这种方式不会删除主键约束和索引
connection.execute(sa.text(f"""
ALTER TABLE "{table_name}"
ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (START WITH {next_value})
"""))
print(f"{table_name} 序列已转换为Identity起始值为 {next_value}")
except Exception as e:
print(f"转换表 {table_name} 序列时出错: {e}")
# 如果是已经存在的Identity错误则忽略
if "already an identity column" in str(e):
print(f"{table_name} 的id列已经是Identity类型忽略此错误")
return
raise

View File

@@ -0,0 +1,21 @@
"""2.1.9
Revision ID: d58298a0879f
Revises: 4666ce24a443
Create Date: 2025-08-19 11:56:39.652032
"""
# revision identifiers, used by Alembic.
revision = 'd58298a0879f'
down_revision = '4666ce24a443'
branch_labels = None
depends_on = None
def upgrade() -> None:
pass
def downgrade() -> None:
pass

View File

@@ -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"]=""
@@ -195,13 +205,16 @@ fi
# 使用 `envsubst` 将模板文件中的 ${NGINX_PORT} 替换为实际的环境变量值
envsubst '${NGINX_PORT}${PORT}${NGINX_CLIENT_MAX_BODY_SIZE}${ENABLE_SSL}${HTTPS_SERVER_CONF}' < /etc/nginx/nginx.template.conf > /etc/nginx/nginx.conf
# 自动更新
cd /
source /usr/local/bin/mp_update.sh
cd /app || exit
# 更改 moviepilot userid 和 groupid
groupmod -o -g "${PGID}" moviepilot
usermod -o -u "${PUID}" moviepilot
# 更改文件权限
chown -R moviepilot:moviepilot \
"${HOME}" \
@@ -211,17 +224,21 @@ chown -R moviepilot:moviepilot \
/var/lib/nginx \
/var/log/nginx
chown moviepilot:moviepilot /etc/hosts /tmp
# 下载浏览器内核
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
# 证书管理
source /app/docker/cert.sh
# 启动前端nginx服务
INFO "→ 启动前端nginx服务..."
nginx
# 启动docker http proxy nginx
if [ -S "/var/run/docker.sock" ]; then
INFO "→ 启动 Docker Proxy..."
@@ -231,6 +248,7 @@ if [ -S "/var/run/docker.sock" ]; then
/var/lib/nginx \
/var/log/nginx
fi
# 设置后端服务权限掩码
umask "${UMASK}"

220
docs/postgresql-setup.md Normal file
View File

@@ -0,0 +1,220 @@
# PostgreSQL 数据库配置指南
MoviePilot 现在支持 PostgreSQL 数据库,您可以根据需要选择使用 SQLite 或 PostgreSQL。
## 配置选项
### 1. 数据库类型选择
`config/app.env` 文件中设置:
```bash
# 使用 SQLite默认
DB_TYPE=sqlite
# 使用 PostgreSQL
DB_TYPE=postgresql
```
### 2. PostgreSQL 配置参数
`DB_TYPE=postgresql` 时,以下配置生效:
```bash
# 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
```
## Docker 部署
### 使用内置 PostgreSQL
如果您使用 Docker 部署MoviePilot 容器内置了 PostgreSQL 服务:
#### 使用 Docker Compose推荐
1. 创建 `docker-compose.yml` 文件:
```yaml
version: '3.8'
services:
moviepilot:
image: jxxghp/moviepilot:latest
container_name: moviepilot
restart: unless-stopped
ports:
- "3000:3000" # 前端端口
- "3001:3001" # API端口
environment:
- DB_TYPE=postgresql
- DB_POSTGRESQL_HOST=localhost
- DB_POSTGRESQL_PORT=5432
- DB_POSTGRESQL_DATABASE=moviepilot
- DB_POSTGRESQL_USERNAME=moviepilot
- DB_POSTGRESQL_PASSWORD=moviepilot
volumes:
- ./config:/config
```
2. 启动服务:
```bash
docker-compose up -d
```
#### 使用 Docker 命令
1. 设置环境变量:
```bash
DB_TYPE=postgresql
```
2. 启动容器时PostgreSQL 服务会自动:
- 在配置目录下创建 `postgresql/` 子目录作为数据目录
- 初始化 PostgreSQL 数据目录
- 启动 PostgreSQL 服务
- 创建数据库和用户
- 配置连接权限
3. 数据持久化:
- PostgreSQL 数据存储在 `${CONFIG_DIR}/postgresql/` 目录中
- 日志文件存储在 `${CONFIG_DIR}/postgresql/logs/` 目录中
- 这些目录会通过 Docker 卷映射持久化保存
### 使用外部 PostgreSQL
如果您想使用外部的 PostgreSQL 服务:
1. 确保外部 PostgreSQL 服务已启动并可访问
2. 设置环境变量指向外部服务:
```bash
DB_TYPE=postgresql
DB_POSTGRESQL_HOST=your-postgresql-host
DB_POSTGRESQL_PORT=5432
DB_POSTGRESQL_DATABASE=moviepilot
DB_POSTGRESQL_USERNAME=your-username
DB_POSTGRESQL_PASSWORD=your-password
```
## 数据迁移
### 从 SQLite 迁移到 PostgreSQL
1. 备份现有的 SQLite 数据库文件(`config/user.db`
2. 修改配置为 PostgreSQL
3. 启动应用,数据库表会自动创建
4. 使用数据库迁移工具或手动导入数据
### 从 PostgreSQL 迁移到 SQLite
1. 导出 PostgreSQL 数据
2. 修改配置为 SQLite
3. 启动应用,数据库表会自动创建
4. 导入数据到 SQLite
## 数据备份
### PostgreSQL 数据备份
PostgreSQL 数据存储在 `${CONFIG_DIR}/postgresql/` 目录中,您可以通过以下方式进行备份:
#### 1. 文件级备份
```bash
# 备份整个PostgreSQL数据目录
tar -czf postgresql_backup_$(date +%Y%m%d_%H%M%S).tar.gz config/postgresql/
```
#### 2. 数据库级备份
```bash
# 进入容器
docker exec -it moviepilot bash
# 使用pg_dump备份
pg_dump -h localhost -U moviepilot -d moviepilot > /config/moviepilot_backup.sql
# 或使用pg_dumpall备份所有数据库
pg_dumpall -h localhost -U moviepilot > /config/all_databases_backup.sql
```
#### 3. 恢复数据
```bash
# 恢复单个数据库
psql -h localhost -U moviepilot -d moviepilot < /config/moviepilot_backup.sql
# 恢复所有数据库
psql -h localhost -U moviepilot < /config/all_databases_backup.sql
```
## 性能优化
### PostgreSQL 优化建议
1. **连接池配置**
- 根据应用负载调整 `DB_POSTGRESQL_POOL_SIZE`
- 设置合适的 `DB_POSTGRESQL_MAX_OVERFLOW`
2. **数据库配置**
- 调整 `shared_buffers`
- 配置 `work_mem`
- 设置合适的 `maintenance_work_mem`
3. **索引优化**
- 为常用查询字段添加索引
- 定期执行 `VACUUM``ANALYZE`
## 故障排除
### 常见问题
1. **连接失败**
- 检查 PostgreSQL 服务是否启动
- 验证连接参数是否正确
- 确认网络连接和防火墙设置
2. **权限问题**
- 确保用户有足够的数据库权限
- 检查 `pg_hba.conf` 配置
3. **性能问题**
- 监控连接池使用情况
- 检查慢查询日志
- 优化数据库配置
### 日志查看
PostgreSQL 相关日志可以在以下位置查看:
- Docker 容器:`${CONFIG_DIR}/postgresql/logs/`
- 系统日志:`journalctl -u postgresql`
## 注意事项
1. **兼容性**PostgreSQL 支持从 MoviePilot v2.0 开始
2. **备份**:建议定期备份数据库
3. **版本**:建议使用 PostgreSQL 12 或更高版本
4. **字符集**:确保使用 UTF-8 字符集
## 技术支持
如果遇到问题,请:
1. 查看应用日志
2. 检查 PostgreSQL 日志
3. 在 GitHub Issues 中报告问题

View File

@@ -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

View File

@@ -1,2 +1,2 @@
APP_VERSION = 'v2.7.2'
FRONTEND_VERSION = 'v2.7.2'
APP_VERSION = 'v2.7.3'
FRONTEND_VERSION = 'v2.7.3'