# Conflicts:
#	tests/fixtures/architecture/runtime-contract-baseline.json
#	uv.lock
This commit is contained in:
jxxghp
2026-08-22 07:21:20 +08:00
36 changed files with 153 additions and 1034 deletions
+7 -19
View File
@@ -8,8 +8,7 @@ on:
- 'pyproject.toml' - 'pyproject.toml'
- 'uv.lock' - 'uv.lock'
- 'docker/Dockerfile' - 'docker/Dockerfile'
- 'docker/entrypoint.sh' - 'docker/**'
- 'docker/update.sh'
- '.github/workflows/dependency-compat.yml' - '.github/workflows/dependency-compat.yml'
push: push:
branches: branches:
@@ -18,8 +17,7 @@ on:
- 'pyproject.toml' - 'pyproject.toml'
- 'uv.lock' - 'uv.lock'
- 'docker/Dockerfile' - 'docker/Dockerfile'
- 'docker/entrypoint.sh' - 'docker/**'
- 'docker/update.sh'
- '.github/workflows/dependency-compat.yml' - '.github/workflows/dependency-compat.yml'
workflow_dispatch: workflow_dispatch:
@@ -41,39 +39,29 @@ jobs:
include: include:
- name: Linux x64 - name: Linux x64
runner: ubuntu-24.04 runner: ubuntu-24.04
python-version: '3.12' python-version: '3.14'
expected-system: Linux
expected-machine: x86_64
- name: Linux x64
runner: ubuntu-24.04
python-version: '3.13'
expected-system: Linux expected-system: Linux
expected-machine: x86_64 expected-machine: x86_64
- name: Linux ARM64 - name: Linux ARM64
runner: ubuntu-24.04-arm runner: ubuntu-24.04-arm
python-version: '3.12' python-version: '3.14'
expected-system: Linux expected-system: Linux
expected-machine: aarch64 expected-machine: aarch64
- name: macOS Intel - name: macOS Intel
runner: macos-15-intel runner: macos-15-intel
python-version: '3.12' python-version: '3.14'
expected-system: Darwin expected-system: Darwin
expected-machine: x86_64 expected-machine: x86_64
- name: macOS ARM - name: macOS ARM
runner: macos-15 runner: macos-15
python-version: '3.12' python-version: '3.14'
expected-system: Darwin expected-system: Darwin
expected-machine: arm64 expected-machine: arm64
- name: Windows x64 - name: Windows x64
runner: windows-2025 runner: windows-2025
python-version: '3.12' python-version: '3.14'
expected-system: Windows expected-system: Windows
expected-machine: AMD64 expected-machine: AMD64
- name: Linux x64
runner: ubuntu-24.04
python-version: '3.14'
expected-system: Linux
expected-machine: x86_64
steps: steps:
- name: Checkout code - name: Checkout code
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with: with:
version: '0.12.5' version: '0.12.5'
python-version: '3.12' python-version: '3.14'
enable-cache: true enable-cache: true
cache-dependency-glob: | cache-dependency-glob: |
pyproject.toml pyproject.toml
+1 -1
View File
@@ -49,7 +49,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
python-version: '3.12' python-version: '3.14'
- name: Set up uv - name: Set up uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
+2 -2
View File
@@ -80,7 +80,7 @@ jobs:
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with: with:
version: '0.12.5' version: '0.12.5'
python-version: '3.12' python-version: '3.14'
enable-cache: true enable-cache: true
cache-dependency-glob: | cache-dependency-glob: |
pyproject.toml pyproject.toml
@@ -107,7 +107,7 @@ jobs:
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with: with:
version: '0.12.5' version: '0.12.5'
python-version: '3.12' python-version: '3.14'
enable-cache: true enable-cache: true
cache-dependency-glob: | cache-dependency-glob: |
pyproject.toml pyproject.toml
+6
View File
@@ -7,6 +7,12 @@ def _filter_third_party_startup_warnings() -> None:
""" """
过滤第三方库在新版 Python 下产生的已知无害启动警告。 过滤第三方库在新版 Python 下产生的已知无害启动警告。
""" """
warnings.filterwarnings(
"ignore",
message=r"'_UnionGenericAlias' is deprecated and slated for removal in Python 3\.17",
category=DeprecationWarning,
module=r"google\.genai\.types",
)
warnings.filterwarnings( warnings.filterwarnings(
"ignore", "ignore",
message=r"invalid escape sequence '\\&'", message=r"invalid escape sequence '\\&'",
+6 -19
View File
@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Any, Union from typing import Any, ClassVar, Union
from app.chain import ChainBase from app.chain import ChainBase
from app.application.configuration import get_configured_system_config as SystemConfigOper from app.application.configuration import get_configured_system_config as SystemConfigOper
@@ -17,6 +17,11 @@ class BaseAction(ABC):
工作流动作基类 工作流动作基类
""" """
# 工作流编辑器和运行管理器读取的动作级静态元数据。
name: ClassVar[str]
description: ClassVar[str]
data: ClassVar[dict[str, Any]]
# 动作ID # 动作ID
_action_id = None _action_id = None
# 完成标志 # 完成标志
@@ -34,24 +39,6 @@ class BaseAction(ABC):
self._message = "" self._message = ""
self.systemconfigoper = SystemConfigOper() self.systemconfigoper = SystemConfigOper()
@classmethod
@property
@abstractmethod
def name(cls) -> str: # noqa
pass
@classmethod
@property
@abstractmethod
def description(cls) -> str: # noqa
pass
@classmethod
@property
@abstractmethod
def data(cls) -> dict: # noqa
pass
@classmethod @classmethod
def get_contract(cls) -> dict: def get_contract(cls) -> dict:
""" """
+3 -14
View File
@@ -40,20 +40,9 @@ class AddDownloadAction(BaseAction):
self._added_downloads = [] self._added_downloads = []
self._has_error = False self._has_error = False
@classmethod name = "添加下载"
@property description = "根据资源列表添加下载任务"
def name(cls) -> str: # noqa data = AddDownloadParams().model_dump()
return "添加下载"
@classmethod
@property
def description(cls) -> str: # noqa
return "根据资源列表添加下载任务"
@classmethod
@property
def data(cls) -> dict: # noqa
return AddDownloadParams().model_dump()
@property @property
def success(self) -> bool: def success(self) -> bool:
+3 -14
View File
@@ -30,20 +30,9 @@ class AddSubscribeAction(BaseAction):
self._added_subscribes = [] self._added_subscribes = []
self._has_error = False self._has_error = False
@classmethod name = "添加订阅"
@property description = "根据媒体列表添加订阅"
def name(cls) -> str: # noqa data = AddSubscribeParams().model_dump()
return "添加订阅"
@classmethod
@property
def description(cls) -> str: # noqa
return "根据媒体列表添加订阅"
@classmethod
@property
def data(cls) -> dict: # noqa
return AddSubscribeParams().model_dump()
@property @property
def success(self) -> bool: def success(self) -> bool:
+3 -14
View File
@@ -27,20 +27,9 @@ class FetchDownloadsAction(BaseAction):
super().__init__(action_id) super().__init__(action_id)
self._downloads = [] self._downloads = []
@classmethod name = "获取下载任务"
@property description = "获取下载队列中的任务状态"
def name(cls) -> str: # noqa data = FetchDownloadsParams().model_dump()
return "获取下载任务"
@classmethod
@property
def description(cls) -> str: # noqa
return "获取下载队列中的任务状态"
@classmethod
@property
def data(cls) -> dict: # noqa
return FetchDownloadsParams().model_dump()
@property @property
def success(self) -> bool: def success(self) -> bool:
+3 -14
View File
@@ -115,20 +115,9 @@ class FetchMediasAction(BaseAction):
if event_data.extra_sources: if event_data.extra_sources:
self.__inner_sources.extend([s.model_dump() for s in event_data.extra_sources]) self.__inner_sources.extend([s.model_dump() for s in event_data.extra_sources])
@classmethod name = "获取媒体数据"
@property description = "获取榜单等媒体数据列表"
def name(cls) -> str: # noqa data = FetchMediasParams().model_dump()
return "获取媒体数据"
@classmethod
@property
def description(cls) -> str: # noqa
return "获取榜单等媒体数据列表"
@classmethod
@property
def data(cls) -> dict: # noqa
return FetchMediasParams().model_dump()
@property @property
def success(self) -> bool: def success(self) -> bool:
+3 -14
View File
@@ -40,20 +40,9 @@ class FetchRssAction(BaseAction):
self._rss_torrents = [] self._rss_torrents = []
self._has_error = False self._has_error = False
@classmethod name = "获取RSS资源"
@property description = "订阅RSS地址获取资源"
def name(cls) -> str: # noqa data = FetchRssParams().model_dump()
return "获取RSS资源"
@classmethod
@property
def description(cls) -> str: # noqa
return "订阅RSS地址获取资源"
@classmethod
@property
def data(cls) -> dict: # noqa
return FetchRssParams().model_dump()
@property @property
def success(self) -> bool: def success(self) -> bool:
+3 -14
View File
@@ -41,20 +41,9 @@ class FetchTorrentsAction(BaseAction):
super().__init__(action_id) super().__init__(action_id)
self._torrents = [] self._torrents = []
@classmethod name = "搜索站点资源"
@property description = "搜索站点种子资源列表"
def name(cls) -> str: # noqa data = FetchTorrentsParams().model_dump()
return "搜索站点资源"
@classmethod
@property
def description(cls) -> str: # noqa
return "搜索站点种子资源列表"
@classmethod
@property
def data(cls) -> dict: # noqa
return FetchTorrentsParams().model_dump()
@property @property
def success(self) -> bool: def success(self) -> bool:
+3 -14
View File
@@ -32,20 +32,9 @@ class FilterMediasAction(BaseAction):
super().__init__(action_id) super().__init__(action_id)
self._medias = [] self._medias = []
@classmethod name = "过滤媒体数据"
@property description = "对媒体数据列表进行过滤"
def name(cls) -> str: # noqa data = FilterMediasParams().model_dump()
return "过滤媒体数据"
@classmethod
@property
def description(cls) -> str: # noqa
return "对媒体数据列表进行过滤"
@classmethod
@property
def data(cls) -> dict: # noqa
return FilterMediasParams().model_dump()
@property @property
def success(self) -> bool: def success(self) -> bool:
+3 -14
View File
@@ -37,20 +37,9 @@ class FilterTorrentsAction(BaseAction):
super().__init__(action_id) super().__init__(action_id)
self._torrents = [] self._torrents = []
@classmethod name = "过滤资源"
@property description = "对资源列表数据进行过滤"
def name(cls) -> str: # noqa data = FilterTorrentsParams().model_dump()
return "过滤资源"
@classmethod
@property
def description(cls) -> str: # noqa
return "对资源列表数据进行过滤"
@classmethod
@property
def data(cls) -> dict: # noqa
return FilterTorrentsParams().model_dump()
@property @property
def success(self) -> bool: def success(self) -> bool:
+3 -14
View File
@@ -27,20 +27,9 @@ class InvokePluginAction(BaseAction):
super().__init__(action_id) super().__init__(action_id)
self._success = False self._success = False
@classmethod name = "调用插件"
@property description = "调用插件提供的动作"
def name(cls) -> str: # noqa data = InvokePluginParams().model_dump()
return "调用插件"
@classmethod
@property
def description(cls) -> str: # noqa
return "调用插件提供的动作"
@classmethod
@property
def data(cls) -> dict: # noqa
return InvokePluginParams().model_dump()
@property @property
def success(self) -> bool: def success(self) -> bool:
+3 -14
View File
@@ -9,20 +9,9 @@ class NoteAction(BaseAction):
contract = {} contract = {}
@classmethod name = "备注"
@property description = "给工作流添加备注"
def name(cls) -> str: # noqa data = {}
return "备注"
@classmethod
@property
def description(cls) -> str: # noqa
return "给工作流添加备注"
@classmethod
@property
def data(cls) -> dict: # noqa
return {}
@property @property
def success(self) -> bool: def success(self) -> bool:
+3 -14
View File
@@ -34,20 +34,9 @@ class ScanFileAction(BaseAction):
self._fileitems = [] self._fileitems = []
self._has_error = False self._has_error = False
@classmethod name = "扫描目录"
@property description = "扫描目录文件到队列"
def name(cls) -> str: # noqa data = ScanFileParams().model_dump()
return "扫描目录"
@classmethod
@property
def description(cls) -> str: # noqa
return "扫描目录文件到队列"
@classmethod
@property
def data(cls) -> dict: # noqa
return ScanFileParams().model_dump()
@property @property
def success(self) -> bool: def success(self) -> bool:
+3 -14
View File
@@ -30,20 +30,9 @@ class ScrapeFileAction(BaseAction):
self._scraped_files = [] self._scraped_files = []
self._has_error = False self._has_error = False
@classmethod name = "刮削文件"
@property description = "刮削媒体信息和图片"
def name(cls) -> str: # noqa data = ScrapeFileParams().model_dump()
return "刮削文件"
@classmethod
@property
def description(cls) -> str: # noqa
return "刮削媒体信息和图片"
@classmethod
@property
def data(cls) -> dict: # noqa
return ScrapeFileParams().model_dump()
@property @property
def success(self) -> bool: def success(self) -> bool:
+3 -14
View File
@@ -19,20 +19,9 @@ class SendEventAction(BaseAction):
contract = {} contract = {}
@classmethod name = "发送事件"
@property description = "发送任务执行事件"
def name(cls) -> str: # noqa data = SendEventParams().model_dump()
return "发送事件"
@classmethod
@property
def description(cls) -> str: # noqa
return "发送任务执行事件"
@classmethod
@property
def data(cls) -> dict: # noqa
return SendEventParams().model_dump()
@property @property
def success(self) -> bool: def success(self) -> bool:
+3 -14
View File
@@ -27,20 +27,9 @@ class SendMessageAction(BaseAction):
def __init__(self, action_id: str): def __init__(self, action_id: str):
super().__init__(action_id) super().__init__(action_id)
@classmethod name = "发送消息"
@property description = "发送任务执行消息"
def name(cls) -> str: # noqa data = SendMessageParams().model_dump()
return "发送消息"
@classmethod
@property
def description(cls) -> str: # noqa
return "发送任务执行消息"
@classmethod
@property
def data(cls) -> dict: # noqa
return SendMessageParams().model_dump()
@property @property
def success(self) -> bool: def success(self) -> bool:
+3 -14
View File
@@ -41,20 +41,9 @@ class TransferFileAction(BaseAction):
self._fileitems = [] self._fileitems = []
self._has_error = False self._has_error = False
@classmethod name = "整理文件"
@property description = "整理队列中的文件"
def name(cls) -> str: # noqa data = TransferFileParams().model_dump()
return "整理文件"
@classmethod
@property
def description(cls) -> str: # noqa
return "整理队列中的文件"
@classmethod
@property
def data(cls) -> dict: # noqa
return TransferFileParams().model_dump()
@property @property
def success(self) -> bool: def success(self) -> bool:
+6 -3
View File
@@ -3,7 +3,10 @@
FROM ghcr.io/astral-sh/uv:0.12.5@sha256:e85be844203885286c60ffad8a858d48afb6c5a5c237ca0e67f12e74b8f174b1 AS uv FROM ghcr.io/astral-sh/uv:0.12.5@sha256:e85be844203885286c60ffad8a858d48afb6c5a5c237ca0e67f12e74b8f174b1 AS uv
FROM python:3.12.13-slim-bookworm AS base FROM rclone/rclone:1.75.0@sha256:b06aed988cf5967de7c25be5925240983981c757f4ed1ac9d2fa659d51d60548 AS rclone
FROM python:3.14.7-slim-trixie@sha256:ce40764625a4ff50df3548277632e7f96c4e77fe75fa848aae9885476e7df5a4 AS base
# 准备外部制品所需的最小工具集 # 准备外部制品所需的最小工具集
@@ -45,7 +48,7 @@ RUN apt-get update \
-o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \ -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \
&& chmod 0644 /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \ && chmod 0644 /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \
&& printf '%s\n' \ && printf '%s\n' \
'deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt bookworm-pgdg main' \ 'deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt trixie-pgdg main' \
> /etc/apt/sources.list.d/pgdg.list \ > /etc/apt/sources.list.d/pgdg.list \
&& apt-get update \ && apt-get update \
&& apt-get install -y --no-install-recommends \ && apt-get install -y --no-install-recommends \
@@ -78,7 +81,6 @@ RUN apt-get update \
libchromaprint-tools \ libchromaprint-tools \
libjemalloc2 \ libjemalloc2 \
&& dpkg-reconfigure --frontend noninteractive tzdata \ && dpkg-reconfigure --frontend noninteractive tzdata \
&& curl https://rclone.org/install.sh | bash \
&& ln -s /usr/lib/*-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so \ && ln -s /usr/lib/*-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so \
&& apt-get autoremove -y \ && apt-get autoremove -y \
&& apt-get clean \ && apt-get clean \
@@ -220,6 +222,7 @@ ENV LD_PRELOAD="/usr/local/lib/libjemalloc.so" \
# 引入支持 amr 编码的静态 ffmpeg # 引入支持 amr 编码的静态 ffmpeg
COPY --from=mwader/static-ffmpeg:8.1.1 /ffmpeg /usr/local/bin/ COPY --from=mwader/static-ffmpeg:8.1.1 /ffmpeg /usr/local/bin/
COPY --from=mwader/static-ffmpeg:8.1.1 /ffprobe /usr/local/bin/ COPY --from=mwader/static-ffmpeg:8.1.1 /ffprobe /usr/local/bin/
COPY --from=rclone /usr/local/bin/rclone /usr/bin/rclone
# python 环境 # python 环境
COPY --from=prepare_venv --chmod=777 ${VENV_PATH} ${VENV_PATH} COPY --from=prepare_venv --chmod=777 ${VENV_PATH} ${VENV_PATH}
+6 -6
View File
@@ -104,7 +104,7 @@ Usage:
Options: Options:
deps: deps:
--python PYTHON 用于创建虚拟环境的 Python 解释器,默认自动选择本地 3.12+ 版本 --python PYTHON 用于创建虚拟环境的 Python 解释器,默认自动选择本地 3.14+ 版本
--venv PATH 虚拟环境目录,默认 ./venv --venv PATH 虚拟环境目录,默认 ./venv
--recreate 删除并重建虚拟环境 --recreate 删除并重建虚拟环境
--config-dir PATH 指定配置目录 --config-dir PATH 指定配置目录
@@ -144,7 +144,7 @@ show_setup_help() {
Usage: moviepilot setup [OPTIONS] Usage: moviepilot setup [OPTIONS]
Options: Options:
--python PYTHON 用于创建虚拟环境的 Python 解释器,默认自动选择本地 3.12+ 版本 --python PYTHON 用于创建虚拟环境的 Python 解释器,默认自动选择本地 3.14+ 版本
--venv PATH 虚拟环境目录,默认 ./venv --venv PATH 虚拟环境目录,默认 ./venv
--recreate 删除并重建虚拟环境 --recreate 删除并重建虚拟环境
--frontend-version TAG 前端版本,默认使用 version.py 中的 FRONTEND_VERSION --frontend-version TAG 前端版本,默认使用 version.py 中的 FRONTEND_VERSION
@@ -190,7 +190,7 @@ Options:
--ref REF 后端 Git 版本,默认 latest --ref REF 后端 Git 版本,默认 latest
--frontend-version TAG 前端版本,默认使用 version.py 中的 FRONTEND_VERSION --frontend-version TAG 前端版本,默认使用 version.py 中的 FRONTEND_VERSION
--node-version VER 本地 Node 运行时版本,默认 20.12.1 --node-version VER 本地 Node 运行时版本,默认 20.12.1
--python PYTHON 用于安装后端依赖的 Python 解释器,默认自动选择本地 3.12+ 版本 --python PYTHON 用于安装后端依赖的 Python 解释器,默认自动选择本地 3.14+ 版本
--venv PATH 虚拟环境目录,默认 ./venv --venv PATH 虚拟环境目录,默认 ./venv
--recreate 删除并重建虚拟环境 --recreate 删除并重建虚拟环境
--skip-resources 更新 all 时跳过资源同步 --skip-resources 更新 all 时跳过资源同步
@@ -258,7 +258,7 @@ python_version_ok() {
local python_bin="$1" local python_bin="$1"
"$python_bin" - <<'PY' >/dev/null 2>&1 "$python_bin" - <<'PY' >/dev/null 2>&1
import sys import sys
raise SystemExit(0 if sys.version_info >= (3, 12) else 1) raise SystemExit(0 if sys.version_info >= (3, 14) else 1)
PY PY
} }
@@ -376,7 +376,7 @@ require_bootstrap_python() {
if [ -n "$python_bin" ]; then if [ -n "$python_bin" ]; then
return 0 return 0
fi fi
echo "未找到可用的 Python 3.12+ 解释器,请先安装 Python 3.12 或更高版本" >&2 echo "未找到可用的 Python 3.14+ 解释器,请先安装 Python 3.14 或更高版本" >&2
exit 1 exit 1
} }
@@ -427,7 +427,7 @@ bootstrap_python_for_args() {
local external_python local external_python
external_python="$(find_external_python "$target_venv" || true)" external_python="$(find_external_python "$target_venv" || true)"
if [ -z "$external_python" ]; then if [ -z "$external_python" ]; then
echo "重建虚拟环境需要 venv 外部的 Python 3.12+ 解释器" >&2 echo "重建虚拟环境需要 venv 外部的 Python 3.14+ 解释器" >&2
return 1 return 1
fi fi
printf '%s\n' "$external_python" printf '%s\n' "$external_python"
+1 -1
View File
@@ -2,7 +2,7 @@
name = "moviepilot" name = "moviepilot"
# MoviePilot 不作为 Python 包发布,产品版本继续由 version.py 管理。 # MoviePilot 不作为 Python 包发布,产品版本继续由 version.py 管理。
version = "0" version = "0"
requires-python = ">=3.12" requires-python = ">=3.14"
dependencies = [ dependencies = [
"aiofiles~=25.1.0", "aiofiles~=25.1.0",
"aioshutil~=1.6", "aioshutil~=1.6",
+2
View File
@@ -9,6 +9,8 @@ asyncio_debug = true
# 仅对「无法在本仓修复根因」的已知上游/三方弃用告警做精确忽略,保持测试输出干净、 # 仅对「无法在本仓修复根因」的已知上游/三方弃用告警做精确忽略,保持测试输出干净、
# 让本仓自身的新告警更醒目。本仓代码引发的告警一律不在此忽略,应在源码/用例处修复。 # 让本仓自身的新告警更醒目。本仓代码引发的告警一律不在此忽略,应在源码/用例处修复。
filterwarnings = filterwarnings =
ignore:'_UnionGenericAlias' is deprecated and slated for removal in Python 3\.17:DeprecationWarning:google\.genai\.types
ignore:invalid escape sequence '\\&':SyntaxWarning:oss2\.api
ignore:datetime.datetime.utcfromtimestamp\(\) is deprecated:DeprecationWarning ignore:datetime.datetime.utcfromtimestamp\(\) is deprecated:DeprecationWarning
ignore:'crypt' is deprecated:DeprecationWarning ignore:'crypt' is deprecated:DeprecationWarning
ignore:'audioop' is deprecated:DeprecationWarning ignore:'audioop' is deprecated:DeprecationWarning
+11 -11
View File
@@ -178,7 +178,7 @@ python_version_ok() {
local python_bin="$1" local python_bin="$1"
"$python_bin" - <<'PY' >/dev/null 2>&1 "$python_bin" - <<'PY' >/dev/null 2>&1
import sys import sys
raise SystemExit(0 if sys.version_info >= (3, 12) else 1) raise SystemExit(0 if sys.version_info >= (3, 14) else 1)
PY PY
} }
@@ -228,20 +228,20 @@ find_uv_python() {
python_install_hint() { python_install_hint() {
case "$OS_NAME" in case "$OS_NAME" in
macOS) macOS)
echo "脚本已尝试自动安装 Git、curl 和 Python 3.12+。" >&2 echo "脚本已尝试自动安装 Git、curl 和 Python 3.14+。" >&2
echo "如果自动安装失败,请先安装 Homebrew,或手动执行:brew install git curl python@3.12" >&2 echo "如果自动安装失败,请先安装 Homebrew,或手动执行:brew install git curl python@3.14" >&2
;; ;;
Linux*) Linux*)
echo "脚本已尝试自动安装 Git、curl 和 Python 3.12+。" >&2 echo "脚本已尝试自动安装 Git、curl 和 Python 3.14+。" >&2
echo "如果自动安装失败,请先安装 Git、curl、Python 3.12+。" >&2 echo "如果自动安装失败,请先安装 Git、curl、Python 3.14+。" >&2
echo "例如 Debian/Ubuntu: sudo apt install git curl python3.12" >&2 echo "例如 Debian/Ubuntu: sudo apt install git curl python3.14" >&2
echo "例如 Fedora/RHEL: sudo dnf install git curl python3.12" >&2 echo "例如 Fedora/RHEL: sudo dnf install git curl python3.14" >&2
;; ;;
Windows) Windows)
echo "推荐在 WSL、Linux 或 macOS 终端中运行此脚本。" >&2 echo "推荐在 WSL、Linux 或 macOS 终端中运行此脚本。" >&2
;; ;;
*) *)
echo "请先安装 Git、curl、Python 3.12+。" >&2 echo "请先安装 Git、curl、Python 3.14+。" >&2
;; ;;
esac esac
} }
@@ -415,11 +415,11 @@ ensure_python() {
return 0 return 0
fi fi
echo "==> 未找到可用的 Python 3.12+,开始自动安装 Python 3.12" echo "==> 未找到可用的 Python 3.14+,开始自动安装 Python 3.14"
uv python install 3.12 uv python install 3.14
PYTHON_BIN="$(find_uv_python "$(command -v uv)" || true)" PYTHON_BIN="$(find_uv_python "$(command -v uv)" || true)"
if [[ -z "$PYTHON_BIN" ]] || ! python_version_ok "$PYTHON_BIN"; then if [[ -z "$PYTHON_BIN" ]] || ! python_version_ok "$PYTHON_BIN"; then
echo "自动安装 Python 3.12 失败。" >&2 echo "自动安装 Python 3.14 失败。" >&2
return 1 return 1
fi fi
} }
+3 -3
View File
@@ -8,11 +8,11 @@ PROJECT_ROOT=""
PYTHON_BIN="" PYTHON_BIN=""
COLLECTOR_PATH="" COLLECTOR_PATH=""
# 判断候选 Python 是否满足 3.11 最低版本。 # 判断候选 Python 是否满足 3.14 最低版本。
python_version_ok() { python_version_ok() {
"$1" - <<'PY' >/dev/null 2>&1 "$1" - <<'PY' >/dev/null 2>&1
import sys import sys
raise SystemExit(0 if sys.version_info >= (3, 11) else 1) raise SystemExit(0 if sys.version_info >= (3, 14) else 1)
PY PY
} }
@@ -40,7 +40,7 @@ find_python() {
"${PROJECT_ROOT:+$PROJECT_ROOT/venv/bin/python}" \ "${PROJECT_ROOT:+$PROJECT_ROOT/venv/bin/python}" \
"${PROJECT_ROOT:+$PROJECT_ROOT/.venv/bin/python}" \ "${PROJECT_ROOT:+$PROJECT_ROOT/.venv/bin/python}" \
"${VIRTUAL_ENV:+$VIRTUAL_ENV/bin/python}" \ "${VIRTUAL_ENV:+$VIRTUAL_ENV/bin/python}" \
python3.13 python3.12 python3.11 python3; do python3.14 python3; do
[[ -n "$candidate" ]] || continue [[ -n "$candidate" ]] || continue
if [[ "$candidate" == */* ]]; then if [[ "$candidate" == */* ]]; then
resolved="$candidate" resolved="$candidate"
+6 -6
View File
@@ -33,8 +33,8 @@ PUBLIC_DIR = ROOT / "public"
RUNTIME_DIR = ROOT / ".runtime" RUNTIME_DIR = ROOT / ".runtime"
NODE_DIR = RUNTIME_DIR / "node" NODE_DIR = RUNTIME_DIR / "node"
INSTALL_ENV_FILE = ROOT / ".moviepilot.env" INSTALL_ENV_FILE = ROOT / ".moviepilot.env"
MIN_PYTHON_VERSION = (3, 12) MIN_PYTHON_VERSION = (3, 14)
SUPPORTED_PYTHON_TEXT = "Python 3.12+" SUPPORTED_PYTHON_TEXT = "Python 3.14+"
UV_VERSION = "0.12.5" UV_VERSION = "0.12.5"
CONFIG_DIR = LEGACY_CONFIG_DIR CONFIG_DIR = LEGACY_CONFIG_DIR
@@ -2722,7 +2722,7 @@ def install_deps(*, python_bin: str, venv_dir: Path, recreate: bool) -> Path:
venv_dir venv_dir
): ):
raise RuntimeError( raise RuntimeError(
"重建虚拟环境需要使用 venv 外部的 Python 3.12+ 解释器。" "重建虚拟环境需要使用 venv 外部的 Python 3.14+ 解释器。"
) )
uv_bin = require_uv() uv_bin = require_uv()
temporary_uv_dir: Optional[TemporaryDirectory[str]] = None temporary_uv_dir: Optional[TemporaryDirectory[str]] = None
@@ -3719,7 +3719,7 @@ def build_parser() -> argparse.ArgumentParser:
install_parser.add_argument( install_parser.add_argument(
"--python", "--python",
default=DEFAULT_BOOTSTRAP_PYTHON, default=DEFAULT_BOOTSTRAP_PYTHON,
help="用于创建虚拟环境的 Python 解释器,默认自动选择本地 3.12+ 版本", help="用于创建虚拟环境的 Python 解释器,默认自动选择本地 3.14+ 版本",
) )
install_parser.add_argument( install_parser.add_argument(
"--venv", default=str(ROOT / "venv"), help="虚拟环境目录" "--venv", default=str(ROOT / "venv"), help="虚拟环境目录"
@@ -3781,7 +3781,7 @@ def build_parser() -> argparse.ArgumentParser:
setup_parser.add_argument( setup_parser.add_argument(
"--python", "--python",
default=DEFAULT_BOOTSTRAP_PYTHON, default=DEFAULT_BOOTSTRAP_PYTHON,
help="用于创建虚拟环境的 Python 解释器,默认自动选择本地 3.12+ 版本", help="用于创建虚拟环境的 Python 解释器,默认自动选择本地 3.14+ 版本",
) )
setup_parser.add_argument("--venv", default=str(ROOT / "venv"), help="虚拟环境目录") setup_parser.add_argument("--venv", default=str(ROOT / "venv"), help="虚拟环境目录")
setup_parser.add_argument( setup_parser.add_argument(
@@ -3853,7 +3853,7 @@ def build_parser() -> argparse.ArgumentParser:
update_parser.add_argument( update_parser.add_argument(
"--python", "--python",
default=DEFAULT_BOOTSTRAP_PYTHON, default=DEFAULT_BOOTSTRAP_PYTHON,
help="用于安装后端依赖的 Python 解释器,默认自动选择本地 3.12+ 版本", help="用于安装后端依赖的 Python 解释器,默认自动选择本地 3.14+ 版本",
) )
update_parser.add_argument( update_parser.add_argument(
"--venv", default=str(ROOT / "venv"), help="虚拟环境目录" "--venv", default=str(ROOT / "venv"), help="虚拟环境目录"
+2 -2
View File
@@ -142,7 +142,7 @@ class BluRayTest(TestCase):
# 测试手动刮削 # 测试手动刮削
logger.debug(f"测试手动刮削 {path}") logger.debug(f"测试手动刮削 {path}")
mock_metadata_nfo.call_count = 0 mock_metadata_nfo.reset_mock()
ScrapingChain().scrape_metadata( ScrapingChain().scrape_metadata(
fileitem=fileitem, meta=meta, mediainfo=mediainfo, overwrite=True fileitem=fileitem, meta=meta, mediainfo=mediainfo, overwrite=True
) )
@@ -151,7 +151,7 @@ class BluRayTest(TestCase):
# 测试自动刮削 # 测试自动刮削
logger.debug(f"测试自动刮削 {path}") logger.debug(f"测试自动刮削 {path}")
mock_metadata_nfo.call_count = 0 mock_metadata_nfo.reset_mock()
ScrapingChain().scrape_metadata_event( ScrapingChain().scrape_metadata_event(
Event( Event(
event_type=EventType.MetadataScrape, event_type=EventType.MetadataScrape,
+5 -3
View File
@@ -5,13 +5,15 @@ from pathlib import Path
def test_runtime_image_installs_postgresql_18_client_from_pgdg() -> None: def test_runtime_image_installs_postgresql_18_client_from_pgdg() -> None:
"""Bookworm 镜像必须从签名的 PGDG 源安装固定主版本客户端。""" """Trixie 镜像必须从签名的 PGDG 源安装固定主版本客户端。"""
dockerfile = ( dockerfile = (
Path(__file__).resolve().parents[1] / "docker" / "Dockerfile" Path(__file__).resolve().parents[1] / "docker" / "Dockerfile"
).read_text(encoding="utf-8") ).read_text(encoding="utf-8")
assert re.search( assert re.search(
r"^FROM python:[^\s]+-slim-bookworm AS base$", dockerfile, re.MULTILINE r"^FROM python:[^\s]+-slim-trixie@sha256:[0-9a-f]{64} AS base$",
dockerfile,
re.MULTILINE,
) )
assert "https://www.postgresql.org/media/keys/ACCC4CF8.asc" in dockerfile assert "https://www.postgresql.org/media/keys/ACCC4CF8.asc" in dockerfile
for curl_option in ( for curl_option in (
@@ -25,7 +27,7 @@ def test_runtime_image_installs_postgresql_18_client_from_pgdg() -> None:
keyring = "/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc" keyring = "/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc"
assert f"chmod 0644 {keyring}" in dockerfile assert f"chmod 0644 {keyring}" in dockerfile
assert f"signed-by={keyring}" in dockerfile assert f"signed-by={keyring}" in dockerfile
assert "https://apt.postgresql.org/pub/repos/apt bookworm-pgdg main" in dockerfile assert "https://apt.postgresql.org/pub/repos/apt trixie-pgdg main" in dockerfile
assert re.search(r"^\s+postgresql-client-18 \\$", dockerfile, re.MULTILINE) assert re.search(r"^\s+postgresql-client-18 \\$", dockerfile, re.MULTILINE)
assert not re.search(r"^\s+postgresql-client \\$", dockerfile, re.MULTILINE) assert not re.search(r"^\s+postgresql-client \\$", dockerfile, re.MULTILINE)
+4 -4
View File
@@ -65,12 +65,12 @@ class LocalSetupConfigDirTests(unittest.TestCase):
with patch.object(module, "get_python_version", return_value=(3, 15, 0)): with patch.object(module, "get_python_version", return_value=(3, 15, 0)):
module.ensure_supported_python("python3.15") module.ensure_supported_python("python3.15")
def test_supported_python_rejects_versions_below_3_12(self): def test_supported_python_rejects_versions_below_3_14(self):
module = load_local_setup_module() module = load_local_setup_module()
with patch.object(module, "get_python_version", return_value=(3, 11, 9)): with patch.object(module, "get_python_version", return_value=(3, 13, 9)):
with self.assertRaisesRegex(RuntimeError, r"Python 3\.12\+"): with self.assertRaisesRegex(RuntimeError, r"Python 3\.14\+"):
module.ensure_supported_python("python3.11") module.ensure_supported_python("python3.13")
def test_install_deps_installs_browser_runtime(self): def test_install_deps_installs_browser_runtime(self):
module = load_local_setup_module() module = load_local_setup_module()
+5 -5
View File
@@ -30,7 +30,7 @@ def test_recreate_commands_use_external_bootstrap_python(tmp_path, arguments):
bin_dir = tmp_path / "bin" bin_dir = tmp_path / "bin"
bin_dir.mkdir() bin_dir.mkdir()
record = tmp_path / "record" record = tmp_path / "record"
external_python = bin_dir / "python3.12" external_python = bin_dir / "python3.14"
external_python.write_text( external_python.write_text(
"#!/bin/sh\n" "#!/bin/sh\n"
"if [ \"$1\" = \"-\" ]; then exit 0; fi\n" "if [ \"$1\" = \"-\" ]; then exit 0; fi\n"
@@ -47,7 +47,7 @@ def test_recreate_commands_use_external_bootstrap_python(tmp_path, arguments):
) )
venv_python.write_text(venv_script, encoding="utf-8") venv_python.write_text(venv_script, encoding="utf-8")
venv_python.chmod(venv_python.stat().st_mode | stat.S_IXUSR) venv_python.chmod(venv_python.stat().st_mode | stat.S_IXUSR)
venv_alias = venv_python.with_name("python3.12") venv_alias = venv_python.with_name("python3.14")
venv_alias.write_text(venv_script, encoding="utf-8") venv_alias.write_text(venv_script, encoding="utf-8")
venv_alias.chmod(venv_alias.stat().st_mode | stat.S_IXUSR) venv_alias.chmod(venv_alias.stat().st_mode | stat.S_IXUSR)
@@ -117,7 +117,7 @@ def test_recreate_accepts_explicit_external_python(tmp_path):
("update", "backend", "--recreate"), ("update", "backend", "--recreate"),
], ],
) )
@pytest.mark.parametrize("python_option", [(), ("--python", "python3.12")]) @pytest.mark.parametrize("python_option", [(), ("--python", "python3.14")])
def test_recreate_excludes_custom_venv_from_external_bootstrap( def test_recreate_excludes_custom_venv_from_external_bootstrap(
tmp_path, arguments, python_option tmp_path, arguments, python_option
): ):
@@ -135,8 +135,8 @@ def test_recreate_excludes_custom_venv_from_external_bootstrap(
external_bin = tmp_path / "external-bin" external_bin = tmp_path / "external-bin"
external_bin.mkdir() external_bin.mkdir()
record = tmp_path / "record" record = tmp_path / "record"
target_python = target_bin / "python3.12" target_python = target_bin / "python3.14"
external_python = external_bin / "python3.12" external_python = external_bin / "python3.14"
for python_path in (target_python, external_python): for python_path in (target_python, external_python):
python_path.write_text( python_path.write_text(
"#!/bin/sh\n" "#!/bin/sh\n"
+16 -1
View File
@@ -5,7 +5,7 @@ import app
def test_app_installs_known_oss2_invalid_escape_warning_filter(): def test_app_installs_known_oss2_invalid_escape_warning_filter():
""" """
app 初始化过滤器应覆盖 oss2 在 Python 3.12 下产生的无转义警告。 app 初始化过滤器应覆盖 oss2 的无转义警告。
""" """
app._filter_third_party_startup_warnings() app._filter_third_party_startup_warnings()
action, message, category, module, lineno = warnings.filters[0] action, message, category, module, lineno = warnings.filters[0]
@@ -15,3 +15,18 @@ def test_app_installs_known_oss2_invalid_escape_warning_filter():
assert category is SyntaxWarning assert category is SyntaxWarning
assert module is None assert module is None
assert lineno == 0 assert lineno == 0
def test_app_installs_google_genai_python314_warning_filter():
"""app 初始化过滤器应覆盖 Google GenAI SDK 的 Python 3.14 弃用警告。"""
app._filter_third_party_startup_warnings()
assert any(
action == "ignore"
and message.match("'_UnionGenericAlias' is deprecated and slated for removal in Python 3.17")
and category is DeprecationWarning
and module is not None
and module.match("google.genai.types")
and lineno == 0
for action, message, category, module, lineno in warnings.filters
)
+6 -14
View File
@@ -192,20 +192,9 @@ def test_execute_with_inputs_maps_contract_inputs_outputs_and_runtime(monkeypatc
"outputs": [{"name": "downloads", "label": "下载任务", "kind": "list"}], "outputs": [{"name": "downloads", "label": "下载任务", "kind": "list"}],
} }
@classmethod name = "契约动作"
@property description = "测试契约动作"
def name(cls) -> str: data = {}
return "契约动作"
@classmethod
@property
def description(cls) -> str:
return "测试契约动作"
@classmethod
@property
def data(cls) -> dict:
return {}
@property @property
def success(self) -> bool: def success(self) -> bool:
@@ -254,5 +243,8 @@ def test_workflow_manager_list_actions_exposes_contract():
actions = manager.list_actions() actions = manager.list_actions()
assert actions[0]["name"] == "获取RSS资源"
assert actions[0]["description"] == "订阅RSS地址获取资源"
assert isinstance(actions[0]["data"], dict)
assert actions[0]["contract"]["outputs"][0]["name"] == "torrents" assert actions[0]["contract"]["outputs"][0]["name"] == "torrents"
assert actions[0]["contract"]["condition_fields"][0]["label"] == "资源" assert actions[0]["contract"]["condition_fields"][0]["label"] == "资源"
Generated
+7 -718
View File
File diff suppressed because it is too large Load Diff