mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 15:38:19 +08:00
fix local cli install and config workflow
This commit is contained in:
@@ -23,6 +23,7 @@ config/temp/
|
|||||||
config/cache/
|
config/cache/
|
||||||
.runtime/
|
.runtime/
|
||||||
public/
|
public/
|
||||||
|
.moviepilot.env
|
||||||
*.pyc
|
*.pyc
|
||||||
*.log
|
*.log
|
||||||
.vscode
|
.vscode
|
||||||
|
|||||||
+110
-43
@@ -1,11 +1,9 @@
|
|||||||
# MoviePilot CLI
|
# MoviePilot CLI
|
||||||
|
|
||||||
`moviepilot` 是 MoviePilot 本地源码模式的一体化入口,用于安装后端、安装前端 release、同步资源文件、初始化配置,以及统一管理前后端服务。
|
`moviepilot` 是 MoviePilot 本地源码模式的一体化入口,负责本地安装、初始化、更新,以及前后端服务管理。
|
||||||
|
|
||||||
## 一键安装
|
## 一键安装
|
||||||
|
|
||||||
直接从仓库读取脚本并执行:
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
curl -fsSL https://raw.githubusercontent.com/jxxghp/MoviePilot/v2/scripts/bootstrap-local.sh | bash
|
curl -fsSL https://raw.githubusercontent.com/jxxghp/MoviePilot/v2/scripts/bootstrap-local.sh | bash
|
||||||
```
|
```
|
||||||
@@ -20,19 +18,44 @@ curl -fsSL https://raw.githubusercontent.com/jxxghp/MoviePilot/v2/scripts/bootst
|
|||||||
- 下载 `MoviePilot-Resources` 主分支资源
|
- 下载 `MoviePilot-Resources` 主分支资源
|
||||||
- 将 `resources.v2/*` 同步到后端 [app/helper](/Users/jxxghp/PycharmProjects/MoviePilot/app/helper)
|
- 将 `resources.v2/*` 同步到后端 [app/helper](/Users/jxxghp/PycharmProjects/MoviePilot/app/helper)
|
||||||
- 下载本地 Node 运行时并安装前端运行依赖
|
- 下载本地 Node 运行时并安装前端运行依赖
|
||||||
|
- 执行初始化向导
|
||||||
- 创建全局 `moviepilot` 命令
|
- 创建全局 `moviepilot` 命令
|
||||||
- 默认启动前后端服务
|
- 默认启动前后端服务
|
||||||
|
|
||||||
|
如果安装完成后当前终端仍提示找不到 `moviepilot`:
|
||||||
|
|
||||||
|
- 重新打开终端
|
||||||
|
- 如果脚本提示使用了 `~/.local/bin`,执行 `source ~/.zshrc` 或 `source ~/.bashrc`
|
||||||
|
|
||||||
|
## 配置目录
|
||||||
|
|
||||||
|
本地 CLI 默认将配置目录放在程序目录外,避免直接删除程序目录时把配置一并删掉。
|
||||||
|
|
||||||
|
- macOS:`~/Library/Application Support/MoviePilot`
|
||||||
|
- Linux:`${XDG_CONFIG_HOME:-~/.config}/moviepilot`
|
||||||
|
|
||||||
|
可以在安装或初始化时手动指定:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
moviepilot setup --config-dir /path/to/moviepilot-config
|
||||||
|
moviepilot init --config-dir /path/to/moviepilot-config
|
||||||
|
```
|
||||||
|
|
||||||
|
查看当前实际配置目录:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
moviepilot config path
|
||||||
|
```
|
||||||
|
|
||||||
## 目录说明
|
## 目录说明
|
||||||
|
|
||||||
本地安装完成后,主要运行目录如下:
|
|
||||||
|
|
||||||
- 后端代码:仓库根目录
|
- 后端代码:仓库根目录
|
||||||
|
- 外置配置目录:`moviepilot config path` 输出的 `Config Dir`
|
||||||
- 前端静态文件:`public/`
|
- 前端静态文件:`public/`
|
||||||
- 前端本地 Node 运行时:`.runtime/node/`
|
- 前端本地 Node 运行时:`.runtime/node/`
|
||||||
- 后端日志:`config/logs/moviepilot.log`
|
- 后端日志:`<Config Dir>/logs/moviepilot.log`
|
||||||
- 后端启动日志:`config/logs/moviepilot.stdout.log`
|
- 后端启动日志:`<Config Dir>/logs/moviepilot.stdout.log`
|
||||||
- 前端启动日志:`config/logs/moviepilot.frontend.stdout.log`
|
- 前端启动日志:`<Config Dir>/logs/moviepilot.frontend.stdout.log`
|
||||||
|
|
||||||
## 帮助与发现
|
## 帮助与发现
|
||||||
|
|
||||||
@@ -50,6 +73,8 @@ moviepilot commands
|
|||||||
moviepilot help install
|
moviepilot help install
|
||||||
moviepilot help init
|
moviepilot help init
|
||||||
moviepilot help setup
|
moviepilot help setup
|
||||||
|
moviepilot help update
|
||||||
|
moviepilot help agent
|
||||||
moviepilot help config
|
moviepilot help config
|
||||||
moviepilot help config set
|
moviepilot help config set
|
||||||
moviepilot help tool
|
moviepilot help tool
|
||||||
@@ -79,6 +104,10 @@ moviepilot install frontend
|
|||||||
moviepilot install resources
|
moviepilot install resources
|
||||||
moviepilot init
|
moviepilot init
|
||||||
moviepilot setup
|
moviepilot setup
|
||||||
|
moviepilot update backend
|
||||||
|
moviepilot update frontend
|
||||||
|
moviepilot update all
|
||||||
|
moviepilot agent
|
||||||
moviepilot start
|
moviepilot start
|
||||||
moviepilot stop
|
moviepilot stop
|
||||||
moviepilot restart
|
moviepilot restart
|
||||||
@@ -109,6 +138,7 @@ moviepilot install deps
|
|||||||
moviepilot install deps --python python3.12
|
moviepilot install deps --python python3.12
|
||||||
moviepilot install deps --venv /path/to/venv
|
moviepilot install deps --venv /path/to/venv
|
||||||
moviepilot install deps --recreate
|
moviepilot install deps --recreate
|
||||||
|
moviepilot install deps --config-dir /path/to/moviepilot-config
|
||||||
```
|
```
|
||||||
|
|
||||||
安装前端 release:
|
安装前端 release:
|
||||||
@@ -118,6 +148,7 @@ moviepilot install frontend
|
|||||||
moviepilot install frontend --version latest
|
moviepilot install frontend --version latest
|
||||||
moviepilot install frontend --version v2.9.31
|
moviepilot install frontend --version v2.9.31
|
||||||
moviepilot install frontend --node-version 20.12.1
|
moviepilot install frontend --node-version 20.12.1
|
||||||
|
moviepilot install frontend --config-dir /path/to/moviepilot-config
|
||||||
```
|
```
|
||||||
|
|
||||||
说明:
|
说明:
|
||||||
@@ -132,6 +163,7 @@ moviepilot install frontend --node-version 20.12.1
|
|||||||
moviepilot install resources
|
moviepilot install resources
|
||||||
moviepilot install resources --resources-repo /path/to/MoviePilot-Resources
|
moviepilot install resources --resources-repo /path/to/MoviePilot-Resources
|
||||||
moviepilot install resources --resource-dir /path/to/resources.v2
|
moviepilot install resources --resource-dir /path/to/resources.v2
|
||||||
|
moviepilot install resources --config-dir /path/to/moviepilot-config
|
||||||
```
|
```
|
||||||
|
|
||||||
说明:
|
说明:
|
||||||
@@ -149,6 +181,7 @@ moviepilot init
|
|||||||
moviepilot init --wizard
|
moviepilot init --wizard
|
||||||
moviepilot init --skip-resources
|
moviepilot init --skip-resources
|
||||||
moviepilot init --force-token
|
moviepilot init --force-token
|
||||||
|
moviepilot init --config-dir /path/to/moviepilot-config
|
||||||
```
|
```
|
||||||
|
|
||||||
一体化安装:
|
一体化安装:
|
||||||
@@ -160,6 +193,7 @@ moviepilot setup --frontend-version latest
|
|||||||
moviepilot setup --node-version 20.12.1
|
moviepilot setup --node-version 20.12.1
|
||||||
moviepilot setup --skip-resources
|
moviepilot setup --skip-resources
|
||||||
moviepilot setup --recreate
|
moviepilot setup --recreate
|
||||||
|
moviepilot setup --config-dir /path/to/moviepilot-config
|
||||||
```
|
```
|
||||||
|
|
||||||
`moviepilot setup` 会串行执行:
|
`moviepilot setup` 会串行执行:
|
||||||
@@ -177,9 +211,60 @@ moviepilot setup --recreate
|
|||||||
- 媒体服务器
|
- 媒体服务器
|
||||||
- 消息通知渠道
|
- 消息通知渠道
|
||||||
|
|
||||||
|
## 更新命令
|
||||||
|
|
||||||
|
更新后端:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
moviepilot update backend
|
||||||
|
moviepilot update backend --ref latest
|
||||||
|
moviepilot update backend --ref v2
|
||||||
|
moviepilot update backend --ref v2.9.31
|
||||||
|
```
|
||||||
|
|
||||||
|
更新前端:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
moviepilot update frontend
|
||||||
|
moviepilot update frontend --frontend-version latest
|
||||||
|
moviepilot update frontend --frontend-version v2.9.31
|
||||||
|
```
|
||||||
|
|
||||||
|
整体更新:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
moviepilot update all
|
||||||
|
moviepilot update all --ref latest --frontend-version latest
|
||||||
|
moviepilot update all --skip-resources
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- `update backend` 会更新 Git 仓库并重新安装后端依赖
|
||||||
|
- `update frontend` 会下载并替换前端 release
|
||||||
|
- `update all` 会同时更新后端、前端,默认也会同步资源文件
|
||||||
|
- 更新前请先执行 `moviepilot stop`
|
||||||
|
|
||||||
|
## Agent 命令
|
||||||
|
|
||||||
|
直接给智能体发送一次请求:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
moviepilot agent 帮我分析最近一次搜索失败的原因
|
||||||
|
moviepilot agent --user-id admin 帮我检查当前下载器配置
|
||||||
|
moviepilot agent --session cli-debug-1 帮我看看为什么没有自动整理
|
||||||
|
moviepilot agent --new-session 帮我总结当前系统配置有什么明显问题
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- `moviepilot agent` 直接在本地环境里发起一次智能体请求
|
||||||
|
- 默认每次可自动创建新会话,也可以通过 `--session` 指定会话 ID
|
||||||
|
- 使用前需要先正确配置 LLM 相关参数,并打开 `AI_AGENT_ENABLE`
|
||||||
|
|
||||||
## 服务管理命令
|
## 服务管理命令
|
||||||
|
|
||||||
`moviepilot start/stop/restart/status` 现在统一管理前后端。
|
`moviepilot start/stop/restart/status` 统一管理前后端。
|
||||||
|
|
||||||
启动、停止、重启与状态:
|
启动、停止、重启与状态:
|
||||||
|
|
||||||
@@ -258,37 +343,39 @@ moviepilot config describe API_TOKEN --show-secrets
|
|||||||
|
|
||||||
- `config list` 显示当前配置值
|
- `config list` 显示当前配置值
|
||||||
- `config keys` 显示配置项名称、类型和默认值
|
- `config keys` 显示配置项名称、类型和默认值
|
||||||
- `config describe` 显示单个配置项的类型、默认值、当前值与配置文件位置
|
- `config describe` 显示单个配置项的类型、默认值和当前值
|
||||||
- 如果前后端正在运行,更新配置后需要 `moviepilot restart`
|
|
||||||
|
|
||||||
## 工具命令
|
## Tool 命令
|
||||||
|
|
||||||
工具命令依赖后端已启动,并且本地配置中存在有效的 `API_TOKEN`。
|
列出所有 MCP 工具:
|
||||||
|
|
||||||
列出工具:
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
moviepilot tool list
|
moviepilot tool list
|
||||||
```
|
```
|
||||||
|
|
||||||
查看工具参数:
|
查看单个工具的参数说明:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
moviepilot tool show search_media
|
moviepilot tool show query_schedulers
|
||||||
|
moviepilot tool show search_torrents
|
||||||
```
|
```
|
||||||
|
|
||||||
调用工具:
|
运行工具:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
moviepilot tool run search_media title="Inception" media_type=movie
|
|
||||||
moviepilot tool run query_schedulers
|
moviepilot tool run query_schedulers
|
||||||
|
moviepilot tool run search_torrents media_type=movie tmdb_id=12345
|
||||||
```
|
```
|
||||||
|
|
||||||
`tool list` 和 `tool show` 是查看“当前后端实际暴露的全部工具与参数”的推荐方式。
|
说明:
|
||||||
|
|
||||||
## 调度命令
|
- `tool list` 用于动态发现当前服务可调用的工具
|
||||||
|
- `tool show` 会输出参数名、类型和描述
|
||||||
|
- `tool run` 参数格式固定为 `key=value`
|
||||||
|
|
||||||
查看调度任务:
|
## Scheduler 命令
|
||||||
|
|
||||||
|
列出调度任务:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
moviepilot scheduler list
|
moviepilot scheduler list
|
||||||
@@ -297,25 +384,5 @@ moviepilot scheduler list
|
|||||||
立即执行调度任务:
|
立即执行调度任务:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
moviepilot scheduler run subscribe_search
|
moviepilot scheduler run subscribe_refresh
|
||||||
```
|
|
||||||
|
|
||||||
## 推荐流程
|
|
||||||
|
|
||||||
首次安装:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
moviepilot setup --wizard
|
|
||||||
moviepilot start
|
|
||||||
moviepilot status
|
|
||||||
```
|
|
||||||
|
|
||||||
日常维护:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
moviepilot status
|
|
||||||
moviepilot logs --frontend
|
|
||||||
moviepilot logs --stdio
|
|
||||||
moviepilot config keys
|
|
||||||
moviepilot tool list
|
|
||||||
```
|
```
|
||||||
|
|||||||
+107
-5
@@ -9,11 +9,13 @@ Usage: moviepilot [BOOTSTRAP COMMAND] | [RUNTIME COMMAND]
|
|||||||
moviepilot commands
|
moviepilot commands
|
||||||
|
|
||||||
Bootstrap Commands:
|
Bootstrap Commands:
|
||||||
moviepilot install deps [--python PYTHON] [--venv PATH] [--recreate]
|
moviepilot install deps [--python PYTHON] [--venv PATH] [--recreate] [--config-dir PATH]
|
||||||
moviepilot install frontend [--version latest] [--node-version 20.12.1]
|
moviepilot install frontend [--version latest] [--node-version 20.12.1] [--config-dir PATH]
|
||||||
moviepilot install resources [--resources-repo PATH] [--resource-dir PATH]
|
moviepilot install resources [--resources-repo PATH] [--resource-dir PATH] [--config-dir PATH]
|
||||||
moviepilot init [--skip-resources] [--force-token] [--wizard]
|
moviepilot init [--skip-resources] [--force-token] [--wizard] [--config-dir PATH]
|
||||||
moviepilot setup [--python PYTHON] [--venv PATH] [--recreate] [--frontend-version latest] [--node-version 20.12.1] [--wizard]
|
moviepilot setup [--python PYTHON] [--venv PATH] [--recreate] [--frontend-version latest] [--node-version 20.12.1] [--wizard] [--config-dir PATH]
|
||||||
|
moviepilot update {backend|frontend|all} [OPTIONS]
|
||||||
|
moviepilot agent [OPTIONS] MESSAGE...
|
||||||
|
|
||||||
Runtime Commands:
|
Runtime Commands:
|
||||||
moviepilot start|stop|restart|status|logs|version
|
moviepilot start|stop|restart|status|logs|version
|
||||||
@@ -25,6 +27,7 @@ Discovery Commands:
|
|||||||
moviepilot help
|
moviepilot help
|
||||||
moviepilot help config
|
moviepilot help config
|
||||||
moviepilot help install
|
moviepilot help install
|
||||||
|
moviepilot help update
|
||||||
moviepilot commands
|
moviepilot commands
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
@@ -32,6 +35,8 @@ Examples:
|
|||||||
moviepilot install frontend
|
moviepilot install frontend
|
||||||
moviepilot install resources
|
moviepilot install resources
|
||||||
moviepilot setup --wizard
|
moviepilot setup --wizard
|
||||||
|
moviepilot update all
|
||||||
|
moviepilot agent 帮我分析最近一次搜索失败
|
||||||
moviepilot help config
|
moviepilot help config
|
||||||
moviepilot config keys
|
moviepilot config keys
|
||||||
moviepilot start
|
moviepilot start
|
||||||
@@ -47,6 +52,10 @@ Bootstrap Commands
|
|||||||
install resources
|
install resources
|
||||||
init
|
init
|
||||||
setup
|
setup
|
||||||
|
update backend
|
||||||
|
update frontend
|
||||||
|
update all
|
||||||
|
agent
|
||||||
|
|
||||||
Runtime Commands
|
Runtime Commands
|
||||||
start
|
start
|
||||||
@@ -85,14 +94,17 @@ Options:
|
|||||||
--python PYTHON 用于创建虚拟环境的 Python 解释器
|
--python PYTHON 用于创建虚拟环境的 Python 解释器
|
||||||
--venv PATH 虚拟环境目录,默认 ./venv
|
--venv PATH 虚拟环境目录,默认 ./venv
|
||||||
--recreate 删除并重建虚拟环境
|
--recreate 删除并重建虚拟环境
|
||||||
|
--config-dir PATH 指定配置目录
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
--version TAG 前端版本,默认 latest
|
--version TAG 前端版本,默认 latest
|
||||||
--node-version VER 本地 Node 运行时版本,默认 20.12.1
|
--node-version VER 本地 Node 运行时版本,默认 20.12.1
|
||||||
|
--config-dir PATH 指定配置目录
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
--resources-repo PATH 本地 MoviePilot-Resources 仓库路径
|
--resources-repo PATH 本地 MoviePilot-Resources 仓库路径
|
||||||
--resource-dir PATH 直接指定 resources.v2 目录
|
--resource-dir PATH 直接指定 resources.v2 目录
|
||||||
|
--config-dir PATH 指定配置目录
|
||||||
|
|
||||||
-h, --help 显示帮助
|
-h, --help 显示帮助
|
||||||
EOF
|
EOF
|
||||||
@@ -106,6 +118,7 @@ Options:
|
|||||||
--skip-resources 跳过资源同步
|
--skip-resources 跳过资源同步
|
||||||
--force-token 强制重置 API_TOKEN
|
--force-token 强制重置 API_TOKEN
|
||||||
--wizard 启动交互式初始化向导
|
--wizard 启动交互式初始化向导
|
||||||
|
--config-dir PATH 指定配置目录
|
||||||
-h, --help 显示帮助
|
-h, --help 显示帮助
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
@@ -123,6 +136,41 @@ Options:
|
|||||||
--skip-resources 跳过资源同步
|
--skip-resources 跳过资源同步
|
||||||
--force-token 强制重置 API_TOKEN
|
--force-token 强制重置 API_TOKEN
|
||||||
--wizard 安装完成后启动交互式初始化向导
|
--wizard 安装完成后启动交互式初始化向导
|
||||||
|
--config-dir PATH 指定配置目录
|
||||||
|
-h, --help 显示帮助
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
show_update_help() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
moviepilot update backend [OPTIONS]
|
||||||
|
moviepilot update frontend [OPTIONS]
|
||||||
|
moviepilot update all [OPTIONS]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--ref REF 后端 Git 版本,默认 latest
|
||||||
|
--frontend-version TAG 前端版本,默认 latest
|
||||||
|
--node-version VER 本地 Node 运行时版本,默认 20.12.1
|
||||||
|
--python PYTHON 用于安装后端依赖的 Python 解释器
|
||||||
|
--venv PATH 虚拟环境目录,默认 ./venv
|
||||||
|
--recreate 删除并重建虚拟环境
|
||||||
|
--skip-resources 更新 all 时跳过资源同步
|
||||||
|
--config-dir PATH 指定配置目录
|
||||||
|
-h, --help 显示帮助
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
show_agent_help() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
moviepilot agent [OPTIONS] MESSAGE...
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--session ID 指定会话 ID
|
||||||
|
--new-session 强制创建新会话
|
||||||
|
--user-id ID 智能体上下文中的用户 ID,默认 cli
|
||||||
|
--config-dir PATH 指定配置目录
|
||||||
-h, --help 显示帮助
|
-h, --help 显示帮助
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
@@ -139,6 +187,22 @@ find_system_python() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default_config_dir() {
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Darwin)
|
||||||
|
printf '%s\n' "$HOME/Library/Application Support/MoviePilot"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf '%s\n' "${XDG_CONFIG_HOME:-$HOME/.config}/moviepilot"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
legacy_config_exists() {
|
||||||
|
local legacy_dir="$ROOT/config"
|
||||||
|
[[ -f "$legacy_dir/app.env" ]] || [[ -f "$legacy_dir/user.db" ]] || [[ -d "$legacy_dir/logs" ]] || [[ -d "$legacy_dir/temp" ]] || [[ -d "$legacy_dir/cache" ]] || [[ -d "$legacy_dir/cookies" ]] || [[ -d "$legacy_dir/sites" ]]
|
||||||
|
}
|
||||||
|
|
||||||
run_runtime_cli() {
|
run_runtime_cli() {
|
||||||
if [ ! -x "$VENV_PYTHON" ]; then
|
if [ ! -x "$VENV_PYTHON" ]; then
|
||||||
echo "未找到项目虚拟环境,请先执行 moviepilot install deps 或 moviepilot setup" >&2
|
echo "未找到项目虚拟环境,请先执行 moviepilot install deps 或 moviepilot setup" >&2
|
||||||
@@ -182,6 +246,14 @@ show_command_help() {
|
|||||||
show_setup_help
|
show_setup_help
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
|
agent)
|
||||||
|
show_agent_help
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
update)
|
||||||
|
show_update_help
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
commands)
|
commands)
|
||||||
show_commands
|
show_commands
|
||||||
exit 0
|
exit 0
|
||||||
@@ -207,6 +279,20 @@ ROOT="$(cd -P "$(dirname "$SOURCE")" && pwd)"
|
|||||||
VENV_PYTHON="$ROOT/venv/bin/python"
|
VENV_PYTHON="$ROOT/venv/bin/python"
|
||||||
SETUP_SCRIPT="$ROOT/scripts/local_setup.py"
|
SETUP_SCRIPT="$ROOT/scripts/local_setup.py"
|
||||||
|
|
||||||
|
if [ -z "${CONFIG_DIR:-}" ] && [ -f "$ROOT/.moviepilot.env" ]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$ROOT/.moviepilot.env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${CONFIG_DIR:-}" ]; then
|
||||||
|
if legacy_config_exists; then
|
||||||
|
CONFIG_DIR="$ROOT/config"
|
||||||
|
else
|
||||||
|
CONFIG_DIR="$(default_config_dir)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
export CONFIG_DIR
|
||||||
|
|
||||||
BOOTSTRAP_PYTHON=""
|
BOOTSTRAP_PYTHON=""
|
||||||
if [ -x "$VENV_PYTHON" ]; then
|
if [ -x "$VENV_PYTHON" ]; then
|
||||||
BOOTSTRAP_PYTHON="$VENV_PYTHON"
|
BOOTSTRAP_PYTHON="$VENV_PYTHON"
|
||||||
@@ -270,6 +356,22 @@ case "${1:-}" in
|
|||||||
fi
|
fi
|
||||||
exec "$BOOTSTRAP_PYTHON" "$SETUP_SCRIPT" setup "$@"
|
exec "$BOOTSTRAP_PYTHON" "$SETUP_SCRIPT" setup "$@"
|
||||||
;;
|
;;
|
||||||
|
update)
|
||||||
|
shift
|
||||||
|
if [ -z "$BOOTSTRAP_PYTHON" ]; then
|
||||||
|
echo "未找到可用的 Python 解释器,请先安装 Python 3" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exec "$BOOTSTRAP_PYTHON" "$SETUP_SCRIPT" update "$@"
|
||||||
|
;;
|
||||||
|
agent)
|
||||||
|
shift
|
||||||
|
if [ -z "$BOOTSTRAP_PYTHON" ]; then
|
||||||
|
echo "未找到可用的 Python 解释器,请先安装 Python 3" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exec "$BOOTSTRAP_PYTHON" "$SETUP_SCRIPT" agent "$@"
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [ ! -x "$VENV_PYTHON" ]; then
|
if [ ! -x "$VENV_PYTHON" ]; then
|
||||||
|
|||||||
+119
-12
@@ -7,11 +7,17 @@ WORKDIR="$PWD"
|
|||||||
APP_DIR_NAME="MoviePilot"
|
APP_DIR_NAME="MoviePilot"
|
||||||
LINK_CLI="true"
|
LINK_CLI="true"
|
||||||
LINK_PATH=""
|
LINK_PATH=""
|
||||||
|
CONFIG_DIR=""
|
||||||
RUN_WIZARD="true"
|
RUN_WIZARD="true"
|
||||||
START_AFTER_INSTALL="true"
|
START_AFTER_INSTALL="true"
|
||||||
NON_INTERACTIVE="false"
|
NON_INTERACTIVE="false"
|
||||||
OS_NAME="Unknown"
|
OS_NAME="Unknown"
|
||||||
PYTHON_BIN=""
|
PYTHON_BIN=""
|
||||||
|
PROMPT_INPUT="/dev/stdin"
|
||||||
|
PROMPT_OUTPUT="/dev/stdout"
|
||||||
|
HAS_TTY="false"
|
||||||
|
PATH_RC_FILE=""
|
||||||
|
PATH_UPDATED="false"
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
@@ -21,6 +27,7 @@ Options:
|
|||||||
--workdir PATH 克隆与安装的目标目录,默认当前目录
|
--workdir PATH 克隆与安装的目标目录,默认当前目录
|
||||||
--app-dir NAME MoviePilot 目录名,默认 ${APP_DIR_NAME}
|
--app-dir NAME MoviePilot 目录名,默认 ${APP_DIR_NAME}
|
||||||
--repo-url URL 主项目仓库地址
|
--repo-url URL 主项目仓库地址
|
||||||
|
--config-dir PATH 配置目录,默认使用程序目录外的系统配置目录
|
||||||
--link-path PATH 全局 moviepilot 软链接位置
|
--link-path PATH 全局 moviepilot 软链接位置
|
||||||
--no-link-cli 安装完成后不创建全局 moviepilot 命令
|
--no-link-cli 安装完成后不创建全局 moviepilot 命令
|
||||||
--no-wizard 跳过 moviepilot setup 的交互式初始化向导
|
--no-wizard 跳过 moviepilot setup 的交互式初始化向导
|
||||||
@@ -31,10 +38,35 @@ Options:
|
|||||||
Examples:
|
Examples:
|
||||||
$(basename "$0")
|
$(basename "$0")
|
||||||
$(basename "$0") --workdir ~/Projects
|
$(basename "$0") --workdir ~/Projects
|
||||||
|
$(basename "$0") --config-dir ~/.config/moviepilot-local
|
||||||
$(basename "$0") --non-interactive --workdir ~/Projects --no-start
|
$(basename "$0") --non-interactive --workdir ~/Projects --no-start
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default_config_dir() {
|
||||||
|
case "$OS_NAME" in
|
||||||
|
macOS)
|
||||||
|
printf '%s\n' "$HOME/Library/Application Support/MoviePilot"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf '%s\n' "${XDG_CONFIG_HOME:-$HOME/.config}/moviepilot"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_prompt_io() {
|
||||||
|
if [[ -t 0 && -t 1 ]]; then
|
||||||
|
HAS_TTY="true"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -r /dev/tty && -w /dev/tty ]]; then
|
||||||
|
PROMPT_INPUT="/dev/tty"
|
||||||
|
PROMPT_OUTPUT="/dev/tty"
|
||||||
|
HAS_TTY="true"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
detect_os() {
|
detect_os() {
|
||||||
local uname_s
|
local uname_s
|
||||||
uname_s="$(uname -s)"
|
uname_s="$(uname -s)"
|
||||||
@@ -64,6 +96,10 @@ detect_os() {
|
|||||||
LINK_PATH="/usr/local/bin/moviepilot"
|
LINK_PATH="/usr/local/bin/moviepilot"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
if [[ -z "$CONFIG_DIR" ]]; then
|
||||||
|
CONFIG_DIR="$(default_config_dir)"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
find_python() {
|
find_python() {
|
||||||
@@ -137,14 +173,15 @@ prompt_text() {
|
|||||||
local answer=""
|
local answer=""
|
||||||
|
|
||||||
if [[ -n "$default_value" ]]; then
|
if [[ -n "$default_value" ]]; then
|
||||||
read -r -p "$label [$default_value]: " answer || true
|
printf '%s [%s]: ' "$label" "$default_value" >"$PROMPT_OUTPUT"
|
||||||
if [[ -z "$answer" ]]; then
|
|
||||||
answer="$default_value"
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
read -r -p "$label: " answer || true
|
printf '%s: ' "$label" >"$PROMPT_OUTPUT"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
IFS= read -r answer <"$PROMPT_INPUT" || true
|
||||||
|
if [[ -z "$answer" ]]; then
|
||||||
|
answer="$default_value"
|
||||||
|
fi
|
||||||
printf '%s\n' "$answer"
|
printf '%s\n' "$answer"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +196,8 @@ prompt_yes_no() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
read -r -p "$label $prompt: " answer || true
|
printf '%s %s: ' "$label" "$prompt" >"$PROMPT_OUTPUT"
|
||||||
|
IFS= read -r answer <"$PROMPT_INPUT" || true
|
||||||
answer="${answer,,}"
|
answer="${answer,,}"
|
||||||
if [[ -z "$answer" ]]; then
|
if [[ -z "$answer" ]]; then
|
||||||
answer="$default_value"
|
answer="$default_value"
|
||||||
@@ -168,16 +206,17 @@ prompt_yes_no() {
|
|||||||
y|yes) return 0 ;;
|
y|yes) return 0 ;;
|
||||||
n|no) return 1 ;;
|
n|no) return 1 ;;
|
||||||
esac
|
esac
|
||||||
echo "请输入 y 或 n。"
|
printf '请输入 y 或 n。\n' >"$PROMPT_OUTPUT"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
run_interactive_guide() {
|
run_interactive_guide() {
|
||||||
echo "==> 当前系统: $OS_NAME"
|
printf '==> 当前系统: %s\n' "$OS_NAME" >"$PROMPT_OUTPUT"
|
||||||
echo "==> 将自动拉取 MoviePilot,并下载前端 release、资源文件与本地 Node 运行时"
|
printf '==> 将自动拉取 MoviePilot,并下载前端 release、资源文件与本地 Node 运行时\n' >"$PROMPT_OUTPUT"
|
||||||
|
|
||||||
WORKDIR="$(prompt_text "安装目录" "$WORKDIR")"
|
WORKDIR="$(prompt_text "安装目录" "$WORKDIR")"
|
||||||
APP_DIR_NAME="$(prompt_text "主项目目录名" "$APP_DIR_NAME")"
|
APP_DIR_NAME="$(prompt_text "主项目目录名" "$APP_DIR_NAME")"
|
||||||
|
CONFIG_DIR="$(prompt_text "配置目录" "$CONFIG_DIR")"
|
||||||
|
|
||||||
if prompt_yes_no "安装过程中进入 MoviePilot 初始化向导" "y"; then
|
if prompt_yes_no "安装过程中进入 MoviePilot 初始化向导" "y"; then
|
||||||
RUN_WIZARD="true"
|
RUN_WIZARD="true"
|
||||||
@@ -211,6 +250,49 @@ ensure_link_path() {
|
|||||||
mkdir -p "$(dirname "$LINK_PATH")"
|
mkdir -p "$(dirname "$LINK_PATH")"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
detect_rc_file() {
|
||||||
|
local shell_name
|
||||||
|
shell_name="$(basename "${SHELL:-}")"
|
||||||
|
case "$shell_name" in
|
||||||
|
zsh)
|
||||||
|
printf '%s\n' "$HOME/.zshrc"
|
||||||
|
;;
|
||||||
|
bash)
|
||||||
|
printf '%s\n' "$HOME/.bashrc"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf '%s\n' "$HOME/.profile"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_path_configured() {
|
||||||
|
if [[ "$LINK_CLI" != "true" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local bin_dir
|
||||||
|
bin_dir="$(dirname "$LINK_PATH")"
|
||||||
|
export PATH="$bin_dir:$PATH"
|
||||||
|
|
||||||
|
if [[ "$bin_dir" != "$HOME/.local/bin" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
PATH_RC_FILE="$(detect_rc_file)"
|
||||||
|
local export_line='export PATH="$HOME/.local/bin:$PATH"'
|
||||||
|
mkdir -p "$(dirname "$PATH_RC_FILE")"
|
||||||
|
touch "$PATH_RC_FILE"
|
||||||
|
|
||||||
|
if ! grep -Fqs "$export_line" "$PATH_RC_FILE"; then
|
||||||
|
{
|
||||||
|
printf '\n# MoviePilot CLI\n'
|
||||||
|
printf '%s\n' "$export_line"
|
||||||
|
} >>"$PATH_RC_FILE"
|
||||||
|
PATH_UPDATED="true"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--workdir)
|
--workdir)
|
||||||
@@ -225,6 +307,10 @@ while [[ $# -gt 0 ]]; do
|
|||||||
REPO_URL="$2"
|
REPO_URL="$2"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
--config-dir)
|
||||||
|
CONFIG_DIR="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
--link-path)
|
--link-path)
|
||||||
LINK_PATH="$2"
|
LINK_PATH="$2"
|
||||||
shift 2
|
shift 2
|
||||||
@@ -258,12 +344,16 @@ while [[ $# -gt 0 ]]; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
detect_os
|
detect_os
|
||||||
|
setup_prompt_io
|
||||||
require_prereqs
|
require_prereqs
|
||||||
ensure_link_path
|
ensure_link_path
|
||||||
|
|
||||||
if [[ "$NON_INTERACTIVE" != "true" && -t 0 && -t 1 ]]; then
|
if [[ "$NON_INTERACTIVE" != "true" && "$HAS_TTY" == "true" ]]; then
|
||||||
run_interactive_guide
|
run_interactive_guide
|
||||||
ensure_link_path
|
ensure_link_path
|
||||||
|
elif [[ "$RUN_WIZARD" == "true" && "$HAS_TTY" != "true" ]]; then
|
||||||
|
echo "==> 未检测到可用终端输入,已跳过初始化向导。安装完成后可手动执行:moviepilot setup --wizard"
|
||||||
|
RUN_WIZARD="false"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "$WORKDIR"
|
mkdir -p "$WORKDIR"
|
||||||
@@ -279,15 +369,20 @@ fi
|
|||||||
|
|
||||||
cd "$APP_DIR"
|
cd "$APP_DIR"
|
||||||
echo "==> 执行本地环境安装与初始化"
|
echo "==> 执行本地环境安装与初始化"
|
||||||
SETUP_ARGS=(setup)
|
SETUP_ARGS=(setup --config-dir "$CONFIG_DIR")
|
||||||
if [[ "$RUN_WIZARD" == "true" ]]; then
|
if [[ "$RUN_WIZARD" == "true" ]]; then
|
||||||
SETUP_ARGS+=(--wizard)
|
SETUP_ARGS+=(--wizard)
|
||||||
fi
|
fi
|
||||||
./moviepilot "${SETUP_ARGS[@]}"
|
if [[ "$HAS_TTY" == "true" ]]; then
|
||||||
|
./moviepilot "${SETUP_ARGS[@]}" <"$PROMPT_INPUT"
|
||||||
|
else
|
||||||
|
./moviepilot "${SETUP_ARGS[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "$LINK_CLI" == "true" ]]; then
|
if [[ "$LINK_CLI" == "true" ]]; then
|
||||||
echo "==> 创建全局 moviepilot 命令到 $LINK_PATH"
|
echo "==> 创建全局 moviepilot 命令到 $LINK_PATH"
|
||||||
ln -sf "$APP_DIR/moviepilot" "$LINK_PATH"
|
ln -sf "$APP_DIR/moviepilot" "$LINK_PATH"
|
||||||
|
ensure_path_configured
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$START_AFTER_INSTALL" == "true" ]]; then
|
if [[ "$START_AFTER_INSTALL" == "true" ]]; then
|
||||||
@@ -300,6 +395,7 @@ cat <<EOF
|
|||||||
|
|
||||||
系统环境: $OS_NAME
|
系统环境: $OS_NAME
|
||||||
项目目录: $APP_DIR
|
项目目录: $APP_DIR
|
||||||
|
配置目录: $CONFIG_DIR
|
||||||
Python 解释器: $PYTHON_BIN
|
Python 解释器: $PYTHON_BIN
|
||||||
CLI 命令: ${LINK_CLI:-false}
|
CLI 命令: ${LINK_CLI:-false}
|
||||||
CLI 路径: ${LINK_PATH:-未创建}
|
CLI 路径: ${LINK_PATH:-未创建}
|
||||||
@@ -308,7 +404,18 @@ CLI 路径: ${LINK_PATH:-未创建}
|
|||||||
moviepilot status
|
moviepilot status
|
||||||
moviepilot logs --frontend
|
moviepilot logs --frontend
|
||||||
moviepilot logs --stdio
|
moviepilot logs --stdio
|
||||||
|
moviepilot config path
|
||||||
|
|
||||||
完整 CLI 文档:
|
完整 CLI 文档:
|
||||||
$APP_DIR/docs/cli.md
|
$APP_DIR/docs/cli.md
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
if [[ "$LINK_CLI" == "true" && "$(dirname "$LINK_PATH")" == "$HOME/.local/bin" ]]; then
|
||||||
|
echo
|
||||||
|
echo "PATH 说明:"
|
||||||
|
if [[ "$PATH_UPDATED" == "true" ]]; then
|
||||||
|
echo " 已将 ~/.local/bin 写入 $PATH_RC_FILE"
|
||||||
|
fi
|
||||||
|
echo " 如果当前终端仍提示找不到 moviepilot,请重新打开终端,或执行:"
|
||||||
|
echo " source ${PATH_RC_FILE:-$HOME/.profile}"
|
||||||
|
fi
|
||||||
|
|||||||
+298
-5
@@ -3,15 +3,18 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import asyncio
|
||||||
import getpass
|
import getpass
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import secrets
|
import secrets
|
||||||
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tarfile
|
import tarfile
|
||||||
|
import uuid
|
||||||
import zipfile
|
import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
@@ -19,16 +22,19 @@ from typing import Any, Optional
|
|||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
CONFIG_DIR = ROOT / "config"
|
LEGACY_CONFIG_DIR = ROOT / "config"
|
||||||
|
HELPER_DIR = ROOT / "app" / "helper"
|
||||||
|
PUBLIC_DIR = ROOT / "public"
|
||||||
|
RUNTIME_DIR = ROOT / ".runtime"
|
||||||
|
NODE_DIR = RUNTIME_DIR / "node"
|
||||||
|
INSTALL_ENV_FILE = ROOT / ".moviepilot.env"
|
||||||
|
|
||||||
|
CONFIG_DIR = LEGACY_CONFIG_DIR
|
||||||
LOG_DIR = CONFIG_DIR / "logs"
|
LOG_DIR = CONFIG_DIR / "logs"
|
||||||
CACHE_DIR = CONFIG_DIR / "cache"
|
CACHE_DIR = CONFIG_DIR / "cache"
|
||||||
TEMP_DIR = CONFIG_DIR / "temp"
|
TEMP_DIR = CONFIG_DIR / "temp"
|
||||||
COOKIE_DIR = CONFIG_DIR / "cookies"
|
COOKIE_DIR = CONFIG_DIR / "cookies"
|
||||||
HELPER_DIR = ROOT / "app" / "helper"
|
|
||||||
ENV_FILE = CONFIG_DIR / "app.env"
|
ENV_FILE = CONFIG_DIR / "app.env"
|
||||||
PUBLIC_DIR = ROOT / "public"
|
|
||||||
RUNTIME_DIR = ROOT / ".runtime"
|
|
||||||
NODE_DIR = RUNTIME_DIR / "node"
|
|
||||||
|
|
||||||
DEFAULT_NODE_VERSION = "20.12.1"
|
DEFAULT_NODE_VERSION = "20.12.1"
|
||||||
FRONTEND_LATEST_API = "https://api.github.com/repos/jxxghp/MoviePilot-Frontend/releases/latest"
|
FRONTEND_LATEST_API = "https://api.github.com/repos/jxxghp/MoviePilot-Frontend/releases/latest"
|
||||||
@@ -56,6 +62,116 @@ NOTIFICATION_SWITCH_TYPES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _default_config_dir() -> Path:
|
||||||
|
if platform.system() == "Darwin":
|
||||||
|
return Path.home() / "Library" / "Application Support" / "MoviePilot"
|
||||||
|
return Path(os.getenv("XDG_CONFIG_HOME") or (Path.home() / ".config")) / "moviepilot"
|
||||||
|
|
||||||
|
|
||||||
|
def _legacy_runtime_config_exists() -> bool:
|
||||||
|
markers = [
|
||||||
|
LEGACY_CONFIG_DIR / "app.env",
|
||||||
|
LEGACY_CONFIG_DIR / "user.db",
|
||||||
|
LEGACY_CONFIG_DIR / "logs",
|
||||||
|
LEGACY_CONFIG_DIR / "temp",
|
||||||
|
LEGACY_CONFIG_DIR / "cache",
|
||||||
|
LEGACY_CONFIG_DIR / "cookies",
|
||||||
|
LEGACY_CONFIG_DIR / "sites",
|
||||||
|
]
|
||||||
|
return any(marker.exists() for marker in markers)
|
||||||
|
|
||||||
|
|
||||||
|
def _read_install_env_config_dir() -> Optional[Path]:
|
||||||
|
if not INSTALL_ENV_FILE.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
for line in INSTALL_ENV_FILE.read_text(encoding="utf-8").splitlines():
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped or stripped.startswith("#") or "=" not in stripped:
|
||||||
|
continue
|
||||||
|
key, value = stripped.split("=", 1)
|
||||||
|
if key.strip() != "CONFIG_DIR":
|
||||||
|
continue
|
||||||
|
return Path(value.strip().strip('"').strip("'")).expanduser()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _set_config_dir(config_dir: Path) -> Path:
|
||||||
|
global CONFIG_DIR, LOG_DIR, CACHE_DIR, TEMP_DIR, COOKIE_DIR, ENV_FILE
|
||||||
|
|
||||||
|
CONFIG_DIR = config_dir.expanduser().resolve()
|
||||||
|
LOG_DIR = CONFIG_DIR / "logs"
|
||||||
|
CACHE_DIR = CONFIG_DIR / "cache"
|
||||||
|
TEMP_DIR = CONFIG_DIR / "temp"
|
||||||
|
COOKIE_DIR = CONFIG_DIR / "cookies"
|
||||||
|
ENV_FILE = CONFIG_DIR / "app.env"
|
||||||
|
os.environ["CONFIG_DIR"] = str(CONFIG_DIR)
|
||||||
|
return CONFIG_DIR
|
||||||
|
|
||||||
|
|
||||||
|
def _write_install_env(config_dir: Path) -> None:
|
||||||
|
INSTALL_ENV_FILE.write_text(
|
||||||
|
f"CONFIG_DIR={shlex.quote(str(config_dir.expanduser().resolve()))}\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _seed_default_config_files(target_dir: Path) -> None:
|
||||||
|
for name in ("category.yaml",):
|
||||||
|
source = LEGACY_CONFIG_DIR / name
|
||||||
|
target = target_dir / name
|
||||||
|
if source.exists() and not target.exists():
|
||||||
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(source, target)
|
||||||
|
|
||||||
|
|
||||||
|
def _migrate_legacy_config_if_needed(target_dir: Path) -> None:
|
||||||
|
target_dir = target_dir.expanduser().resolve()
|
||||||
|
if target_dir == LEGACY_CONFIG_DIR.resolve():
|
||||||
|
return
|
||||||
|
if not _legacy_runtime_config_exists():
|
||||||
|
_seed_default_config_files(target_dir)
|
||||||
|
return
|
||||||
|
|
||||||
|
target_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
for source in sorted(LEGACY_CONFIG_DIR.iterdir()):
|
||||||
|
target = target_dir / source.name
|
||||||
|
if target.exists():
|
||||||
|
continue
|
||||||
|
if source.is_dir():
|
||||||
|
shutil.copytree(source, target)
|
||||||
|
else:
|
||||||
|
shutil.copy2(source, target)
|
||||||
|
print_step(f"已将现有本地配置迁移到 {target_dir}")
|
||||||
|
|
||||||
|
|
||||||
|
def configure_config_dir(explicit: Optional[Path] = None, *, persist: bool = False, prefer_external: bool = False) -> Path:
|
||||||
|
if explicit:
|
||||||
|
config_dir = explicit.expanduser().resolve()
|
||||||
|
elif os.getenv("CONFIG_DIR"):
|
||||||
|
config_dir = Path(os.environ["CONFIG_DIR"]).expanduser().resolve()
|
||||||
|
else:
|
||||||
|
install_env_dir = _read_install_env_config_dir()
|
||||||
|
if install_env_dir:
|
||||||
|
config_dir = install_env_dir.resolve()
|
||||||
|
elif prefer_external:
|
||||||
|
config_dir = _default_config_dir().resolve()
|
||||||
|
elif _legacy_runtime_config_exists():
|
||||||
|
config_dir = LEGACY_CONFIG_DIR.resolve()
|
||||||
|
else:
|
||||||
|
config_dir = _default_config_dir().resolve()
|
||||||
|
|
||||||
|
_set_config_dir(config_dir)
|
||||||
|
if prefer_external:
|
||||||
|
_migrate_legacy_config_if_needed(config_dir)
|
||||||
|
if persist:
|
||||||
|
_write_install_env(config_dir)
|
||||||
|
return config_dir
|
||||||
|
|
||||||
|
|
||||||
|
configure_config_dir()
|
||||||
|
|
||||||
|
|
||||||
def print_step(message: str) -> None:
|
def print_step(message: str) -> None:
|
||||||
print(f"==> {message}")
|
print(f"==> {message}")
|
||||||
|
|
||||||
@@ -93,6 +209,7 @@ def ensure_supported_python(python_bin: str) -> None:
|
|||||||
def ensure_local_dirs() -> None:
|
def ensure_local_dirs() -> None:
|
||||||
for path in (CONFIG_DIR, LOG_DIR, CACHE_DIR, TEMP_DIR, COOKIE_DIR, RUNTIME_DIR):
|
for path in (CONFIG_DIR, LOG_DIR, CACHE_DIR, TEMP_DIR, COOKIE_DIR, RUNTIME_DIR):
|
||||||
path.mkdir(parents=True, exist_ok=True)
|
path.mkdir(parents=True, exist_ok=True)
|
||||||
|
_seed_default_config_files(CONFIG_DIR)
|
||||||
|
|
||||||
|
|
||||||
def _load_env_lines() -> list[str]:
|
def _load_env_lines() -> list[str]:
|
||||||
@@ -888,6 +1005,120 @@ def install_deps(*, python_bin: str, venv_dir: Path, recreate: bool) -> Path:
|
|||||||
return venv_python
|
return venv_python
|
||||||
|
|
||||||
|
|
||||||
|
def _read_runtime_file(path: Path) -> Optional[dict[str, Any]]:
|
||||||
|
if not path.exists():
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, json.JSONDecodeError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _pid_exists(pid: int) -> bool:
|
||||||
|
try:
|
||||||
|
os.kill(pid, 0)
|
||||||
|
except ProcessLookupError:
|
||||||
|
return False
|
||||||
|
except PermissionError:
|
||||||
|
return True
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _services_running() -> list[str]:
|
||||||
|
running: list[str] = []
|
||||||
|
runtime_files = {
|
||||||
|
"backend": TEMP_DIR / "moviepilot.runtime.json",
|
||||||
|
"frontend": TEMP_DIR / "moviepilot.frontend.runtime.json",
|
||||||
|
}
|
||||||
|
for name, runtime_file in runtime_files.items():
|
||||||
|
payload = _read_runtime_file(runtime_file)
|
||||||
|
pid = payload.get("pid") if isinstance(payload, dict) else None
|
||||||
|
if pid and _pid_exists(int(pid)):
|
||||||
|
running.append(name)
|
||||||
|
return running
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_services_stopped() -> None:
|
||||||
|
running = _services_running()
|
||||||
|
if running:
|
||||||
|
raise RuntimeError(
|
||||||
|
"检测到本地服务仍在运行(%s),请先执行 `moviepilot stop` 后再更新。"
|
||||||
|
% ", ".join(running)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _git_output(*args: str) -> str:
|
||||||
|
return capture(["git", *args], cwd=ROOT)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_git_clean() -> None:
|
||||||
|
status = _git_output("status", "--porcelain")
|
||||||
|
if status.strip():
|
||||||
|
raise RuntimeError("检测到当前仓库有未提交改动,请先提交或清理后再执行更新。")
|
||||||
|
|
||||||
|
|
||||||
|
def _update_backend_ref(ref: str) -> str:
|
||||||
|
if not (ROOT / ".git").exists():
|
||||||
|
raise RuntimeError("当前目录不是 Git 仓库,无法更新后端代码。")
|
||||||
|
|
||||||
|
_ensure_git_clean()
|
||||||
|
print_step("获取远端更新")
|
||||||
|
run(["git", "fetch", "--tags", "origin"], cwd=ROOT)
|
||||||
|
|
||||||
|
current_branch = _git_output("rev-parse", "--abbrev-ref", "HEAD")
|
||||||
|
if ref == "latest":
|
||||||
|
if current_branch == "HEAD":
|
||||||
|
raise RuntimeError("当前仓库处于 detached HEAD 状态,请使用 `moviepilot update backend --ref <tag|branch>` 指定版本。")
|
||||||
|
print_step(f"更新后端代码到当前分支最新版本:{current_branch}")
|
||||||
|
run(["git", "pull", "--ff-only", "origin", current_branch], cwd=ROOT)
|
||||||
|
return current_branch
|
||||||
|
|
||||||
|
print_step(f"切换后端代码到指定版本:{ref}")
|
||||||
|
run(["git", "checkout", ref], cwd=ROOT)
|
||||||
|
return ref
|
||||||
|
|
||||||
|
|
||||||
|
def update_backend(*, ref: str, python_bin: str, venv_dir: Path, recreate: bool) -> Path:
|
||||||
|
ensure_services_stopped()
|
||||||
|
resolved_ref = _update_backend_ref(ref=ref)
|
||||||
|
venv_python = install_deps(python_bin=python_bin, venv_dir=venv_dir, recreate=recreate)
|
||||||
|
print_step(f"后端更新完成:{resolved_ref}")
|
||||||
|
return venv_python
|
||||||
|
|
||||||
|
|
||||||
|
def run_agent_request(*, message: str, session_id: Optional[str], new_session: bool, user_id: str) -> dict[str, str]:
|
||||||
|
if str(ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
try:
|
||||||
|
from app.db.init import init_db, update_db
|
||||||
|
from app.agent import MoviePilotAgent
|
||||||
|
from app.core.config import settings
|
||||||
|
except ModuleNotFoundError as exc:
|
||||||
|
raise RuntimeError("当前环境尚未安装 MoviePilot 运行依赖,请先执行 moviepilot install deps 或 moviepilot setup") from exc
|
||||||
|
|
||||||
|
if not settings.AI_AGENT_ENABLE:
|
||||||
|
raise RuntimeError("MoviePilot 智能体未启用,请先在配置中打开 AI_AGENT_ENABLE")
|
||||||
|
|
||||||
|
init_db()
|
||||||
|
update_db()
|
||||||
|
|
||||||
|
session = (session_id or "").strip()
|
||||||
|
if new_session or not session:
|
||||||
|
session = f"cli-{uuid.uuid4().hex[:12]}"
|
||||||
|
|
||||||
|
async def _run_agent() -> dict[str, str]:
|
||||||
|
agent = MoviePilotAgent(session_id=session, user_id=user_id or "cli")
|
||||||
|
agent.suppress_user_reply = True
|
||||||
|
await agent.process(message.strip())
|
||||||
|
return {
|
||||||
|
"session_id": session,
|
||||||
|
"result": (agent._streamed_output or "").strip(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return asyncio.run(_run_agent())
|
||||||
|
|
||||||
|
|
||||||
def build_parser() -> argparse.ArgumentParser:
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
parser = argparse.ArgumentParser(description="MoviePilot 本地安装与初始化工具")
|
parser = argparse.ArgumentParser(description="MoviePilot 本地安装与初始化工具")
|
||||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||||
@@ -896,14 +1127,17 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
install_parser.add_argument("--python", default=sys.executable, help="用于创建虚拟环境的 Python 解释器")
|
install_parser.add_argument("--python", default=sys.executable, help="用于创建虚拟环境的 Python 解释器")
|
||||||
install_parser.add_argument("--venv", default=str(ROOT / "venv"), help="虚拟环境目录")
|
install_parser.add_argument("--venv", default=str(ROOT / "venv"), help="虚拟环境目录")
|
||||||
install_parser.add_argument("--recreate", action="store_true", help="删除并重建虚拟环境")
|
install_parser.add_argument("--recreate", action="store_true", help="删除并重建虚拟环境")
|
||||||
|
install_parser.add_argument("--config-dir", help="配置目录,默认使用程序目录外的系统配置目录")
|
||||||
|
|
||||||
frontend_parser = subparsers.add_parser("install-frontend", help="下载前端 release 并安装本地运行时")
|
frontend_parser = subparsers.add_parser("install-frontend", help="下载前端 release 并安装本地运行时")
|
||||||
frontend_parser.add_argument("--version", default="latest", help="前端版本,默认 latest")
|
frontend_parser.add_argument("--version", default="latest", help="前端版本,默认 latest")
|
||||||
frontend_parser.add_argument("--node-version", default=DEFAULT_NODE_VERSION, help="本地 Node 运行时版本")
|
frontend_parser.add_argument("--node-version", default=DEFAULT_NODE_VERSION, help="本地 Node 运行时版本")
|
||||||
|
frontend_parser.add_argument("--config-dir", help="配置目录,默认使用程序目录外的系统配置目录")
|
||||||
|
|
||||||
resources_parser = subparsers.add_parser("install-resources", help="下载资源文件并同步到 app/helper")
|
resources_parser = subparsers.add_parser("install-resources", help="下载资源文件并同步到 app/helper")
|
||||||
resources_parser.add_argument("--resources-repo", help="本地 MoviePilot-Resources 仓库路径")
|
resources_parser.add_argument("--resources-repo", help="本地 MoviePilot-Resources 仓库路径")
|
||||||
resources_parser.add_argument("--resource-dir", help="直接指定 resources.v2 目录")
|
resources_parser.add_argument("--resource-dir", help="直接指定 resources.v2 目录")
|
||||||
|
resources_parser.add_argument("--config-dir", help="配置目录,默认使用程序目录外的系统配置目录")
|
||||||
|
|
||||||
init_parser = subparsers.add_parser("init", help="初始化本地配置与资源文件")
|
init_parser = subparsers.add_parser("init", help="初始化本地配置与资源文件")
|
||||||
init_parser.add_argument("--resources-repo", help="本地 MoviePilot-Resources 仓库路径")
|
init_parser.add_argument("--resources-repo", help="本地 MoviePilot-Resources 仓库路径")
|
||||||
@@ -911,6 +1145,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
init_parser.add_argument("--skip-resources", action="store_true", help="只初始化配置,不同步资源文件")
|
init_parser.add_argument("--skip-resources", action="store_true", help="只初始化配置,不同步资源文件")
|
||||||
init_parser.add_argument("--force-token", action="store_true", help="强制重置 API_TOKEN")
|
init_parser.add_argument("--force-token", action="store_true", help="强制重置 API_TOKEN")
|
||||||
init_parser.add_argument("--wizard", action="store_true", help="启动交互式初始化向导")
|
init_parser.add_argument("--wizard", action="store_true", help="启动交互式初始化向导")
|
||||||
|
init_parser.add_argument("--config-dir", help="配置目录,默认使用程序目录外的系统配置目录")
|
||||||
|
|
||||||
setup_parser = subparsers.add_parser("setup", help="执行 install-deps、install-frontend、install-resources 和 init")
|
setup_parser = subparsers.add_parser("setup", help="执行 install-deps、install-frontend、install-resources 和 init")
|
||||||
setup_parser.add_argument("--python", default=sys.executable, help="用于创建虚拟环境的 Python 解释器")
|
setup_parser.add_argument("--python", default=sys.executable, help="用于创建虚拟环境的 Python 解释器")
|
||||||
@@ -923,6 +1158,25 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
setup_parser.add_argument("--skip-resources", action="store_true", help="只初始化配置,不同步资源文件")
|
setup_parser.add_argument("--skip-resources", action="store_true", help="只初始化配置,不同步资源文件")
|
||||||
setup_parser.add_argument("--force-token", action="store_true", help="强制重置 API_TOKEN")
|
setup_parser.add_argument("--force-token", action="store_true", help="强制重置 API_TOKEN")
|
||||||
setup_parser.add_argument("--wizard", action="store_true", help="安装完成后启动交互式初始化向导")
|
setup_parser.add_argument("--wizard", action="store_true", help="安装完成后启动交互式初始化向导")
|
||||||
|
setup_parser.add_argument("--config-dir", help="配置目录,默认使用程序目录外的系统配置目录")
|
||||||
|
|
||||||
|
agent_parser = subparsers.add_parser("agent", help="直接向 MoviePilot 智能体发送一次请求")
|
||||||
|
agent_parser.add_argument("message", nargs="+", help="发给智能体的文本请求")
|
||||||
|
agent_parser.add_argument("--session", help="会话 ID,默认自动生成")
|
||||||
|
agent_parser.add_argument("--new-session", action="store_true", help="忽略传入会话,强制创建新会话")
|
||||||
|
agent_parser.add_argument("--user-id", default="cli", help="智能体上下文中的用户 ID")
|
||||||
|
agent_parser.add_argument("--config-dir", help="配置目录,默认使用程序目录外的系统配置目录")
|
||||||
|
|
||||||
|
update_parser = subparsers.add_parser("update", help="更新本地后端、前端或全部组件")
|
||||||
|
update_parser.add_argument("target", choices=["backend", "frontend", "all"], help="更新目标")
|
||||||
|
update_parser.add_argument("--ref", default="latest", help="后端 Git 版本,默认 latest")
|
||||||
|
update_parser.add_argument("--frontend-version", default="latest", help="前端版本,默认 latest")
|
||||||
|
update_parser.add_argument("--node-version", default=DEFAULT_NODE_VERSION, help="本地 Node 运行时版本")
|
||||||
|
update_parser.add_argument("--python", default=sys.executable, help="用于安装后端依赖的 Python 解释器")
|
||||||
|
update_parser.add_argument("--venv", default=str(ROOT / "venv"), help="虚拟环境目录")
|
||||||
|
update_parser.add_argument("--recreate", action="store_true", help="删除并重建虚拟环境")
|
||||||
|
update_parser.add_argument("--skip-resources", action="store_true", help="更新 all 时跳过资源同步")
|
||||||
|
update_parser.add_argument("--config-dir", help="配置目录,默认使用程序目录外的系统配置目录")
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
@@ -930,6 +1184,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
def main() -> int:
|
def main() -> int:
|
||||||
parser = build_parser()
|
parser = build_parser()
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
explicit_config_dir = Path(args.config_dir) if getattr(args, "config_dir", None) else None
|
||||||
|
config_dir = configure_config_dir(
|
||||||
|
explicit=explicit_config_dir,
|
||||||
|
persist=True,
|
||||||
|
prefer_external=True,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if args.command == "install-deps":
|
if args.command == "install-deps":
|
||||||
@@ -939,6 +1199,7 @@ def main() -> int:
|
|||||||
recreate=args.recreate,
|
recreate=args.recreate,
|
||||||
)
|
)
|
||||||
print_step(f"后端依赖安装完成,可执行:{venv_python} -m app.cli")
|
print_step(f"后端依赖安装完成,可执行:{venv_python} -m app.cli")
|
||||||
|
print_step(f"当前配置目录:{config_dir}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
if args.command == "install-frontend":
|
if args.command == "install-frontend":
|
||||||
@@ -963,6 +1224,7 @@ def main() -> int:
|
|||||||
wizard=args.wizard,
|
wizard=args.wizard,
|
||||||
)
|
)
|
||||||
print_step("初始化完成")
|
print_step("初始化完成")
|
||||||
|
print_step(f"当前配置目录:{config_dir}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
if args.command == "setup":
|
if args.command == "setup":
|
||||||
@@ -988,6 +1250,37 @@ def main() -> int:
|
|||||||
wizard=args.wizard,
|
wizard=args.wizard,
|
||||||
)
|
)
|
||||||
print_step(f"本地环境已完成安装与初始化:{venv_python}")
|
print_step(f"本地环境已完成安装与初始化:{venv_python}")
|
||||||
|
print_step(f"当前配置目录:{config_dir}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.command == "agent":
|
||||||
|
result = run_agent_request(
|
||||||
|
message=" ".join(args.message),
|
||||||
|
session_id=args.session,
|
||||||
|
new_session=args.new_session,
|
||||||
|
user_id=args.user_id,
|
||||||
|
)
|
||||||
|
if result.get("session_id"):
|
||||||
|
print_step(f"智能体会话:{result['session_id']}")
|
||||||
|
print(result.get("result") or "")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.command == "update":
|
||||||
|
ensure_services_stopped()
|
||||||
|
if args.target in {"backend", "all"}:
|
||||||
|
update_backend(
|
||||||
|
ref=args.ref,
|
||||||
|
python_bin=args.python,
|
||||||
|
venv_dir=Path(args.venv),
|
||||||
|
recreate=args.recreate,
|
||||||
|
)
|
||||||
|
if args.target in {"frontend", "all"}:
|
||||||
|
frontend_result = install_frontend(frontend_version=args.frontend_version, node_version=args.node_version)
|
||||||
|
print_step(f"前端更新完成,版本:{frontend_result['version']}")
|
||||||
|
if args.target == "all" and not args.skip_resources:
|
||||||
|
install_resources(resources_repo=None, resource_dir=None)
|
||||||
|
print_step("资源文件已同步到最新")
|
||||||
|
print_step(f"更新完成,当前配置目录:{config_dir}")
|
||||||
return 0
|
return 0
|
||||||
except subprocess.CalledProcessError as exc:
|
except subprocess.CalledProcessError as exc:
|
||||||
print(f"命令执行失败,退出码:{exc.returncode}", file=sys.stderr)
|
print(f"命令执行失败,退出码:{exc.returncode}", file=sys.stderr)
|
||||||
|
|||||||
Reference in New Issue
Block a user