feat(plugin): sync default markets from wiki at release

This commit is contained in:
jxxghp
2026-08-06 09:15:30 +08:00
parent 4b1df72a4a
commit 7fe7be6d71
10 changed files with 439 additions and 104 deletions

View File

@@ -71,6 +71,7 @@ test_*
# Build artifacts
build/
.build/
dist/
*.egg-info/
rust/**/target/

View File

@@ -2,6 +2,10 @@ name: MoviePilot Builder Beta
on:
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
Docker-build:
runs-on: ubuntu-latest
@@ -16,6 +20,25 @@ jobs:
app_version=$(cat version.py |sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp")
echo "app_version=$app_version" >> $GITHUB_ENV
- name: Checkout Wiki Plugin Market
uses: actions/checkout@v4
with:
repository: jxxghp/MoviePilot-Wiki
ref: main
path: .build/moviepilot-wiki
sparse-checkout: plugin.md
sparse-checkout-cone-mode: false
persist-credentials: false
- name: Generate Plugin Market Default
id: plugin_market
run: |
python3 -m scripts.generate_plugin_market_default \
--wiki-file .build/moviepilot-wiki/plugin.md \
--config-file app/core/config.py
wiki_commit=$(git -C .build/moviepilot-wiki rev-parse HEAD)
echo "wiki_commit=$wiki_commit" >> "$GITHUB_OUTPUT"
- name: Docker Meta
id: meta
uses: docker/metadata-action@v5
@@ -55,6 +78,8 @@ jobs:
linux/arm64/v8
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
labels: |
${{ steps.meta.outputs.labels }}
org.moviepilot.plugin-market-wiki-revision=${{ steps.plugin_market.outputs.wiki_commit }}
cache-from: type=gha,scope=moviepilot-docker,version=2
cache-to: type=gha,scope=moviepilot-docker,mode=max,version=2

View File

@@ -7,6 +7,10 @@ on:
paths:
- 'version.py'
permissions:
contents: write
packages: write
jobs:
Docker-build:
runs-on: ubuntu-latest
@@ -23,6 +27,39 @@ jobs:
run: |
app_version=$(cat version.py |sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp")
echo "app_version=$app_version" >> $GITHUB_ENV
echo "SOURCE_COMMIT=$(git rev-parse HEAD)" >> $GITHUB_ENV
- name: Checkout Wiki Plugin Market
uses: actions/checkout@v4
with:
repository: jxxghp/MoviePilot-Wiki
ref: main
path: .build/moviepilot-wiki
sparse-checkout: plugin.md
sparse-checkout-cone-mode: false
persist-credentials: false
- name: Generate Plugin Market Default
id: plugin_market
run: |
python3 -m scripts.generate_plugin_market_default \
--wiki-file .build/moviepilot-wiki/plugin.md \
--config-file app/core/config.py
wiki_commit=$(git -C .build/moviepilot-wiki rev-parse HEAD)
echo "wiki_commit=$wiki_commit" >> "$GITHUB_OUTPUT"
- name: Create Release Snapshot
id: release_snapshot
env:
WIKI_COMMIT: ${{ steps.plugin_market.outputs.wiki_commit }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add app/core/config.py
if ! git diff --cached --quiet; then
git commit -m "build(plugin-market): sync default from MoviePilot-Wiki@${WIKI_COMMIT:0:12}"
fi
echo "release_commit=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Docker Meta
id: meta
@@ -65,7 +102,10 @@ jobs:
linux/arm64/v8
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
labels: |
${{ steps.meta.outputs.labels }}
org.opencontainers.image.revision=${{ steps.release_snapshot.outputs.release_commit }}
org.moviepilot.plugin-market-wiki-revision=${{ steps.plugin_market.outputs.wiki_commit }}
cache-from: type=gha,scope=moviepilot-docker,version=2
cache-to: type=gha,scope=moviepilot-docker,mode=max,version=2
@@ -78,9 +118,9 @@ jobs:
# 使用 || 作为分隔符,同时获取 commit 消息和作者 GitHub 用户名
if [ -z "$PREVIOUS_TAG" ]; then
COMMITS=$(git log --pretty=format:"%s||%an" HEAD)
COMMITS=$(git log --pretty=format:"%s||%an" "${SOURCE_COMMIT}")
else
COMMITS=$(git log --pretty=format:"%s||%an" ${PREVIOUS_TAG}..HEAD)
COMMITS=$(git log --pretty=format:"%s||%an" "${PREVIOUS_TAG}..${SOURCE_COMMIT}")
fi
# 分类收集 commit 消息(使用关联数组去重)
@@ -188,6 +228,17 @@ jobs:
delete_release: true
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: Publish Release Tag
env:
RELEASE_COMMIT: ${{ steps.release_snapshot.outputs.release_commit }}
run: |
tag_name="v${{ env.app_version }}"
if git show-ref --verify --quiet "refs/tags/${tag_name}"; then
git tag -d "$tag_name"
fi
git tag "$tag_name" "$RELEASE_COMMIT"
git push origin "refs/tags/${tag_name}"
- name: Generate Release
uses: softprops/action-gh-release@v2
with:

View File

@@ -36,6 +36,12 @@ from app.db.user_oper import (
)
from app.helper.image import ImageHelper
from app.helper.locale import LocaleHelper
from app.helper.market import (
PLUGIN_MARKET_WIKI_URL,
extract_plugin_market_repos_from_wiki,
merge_plugin_market_repos,
split_plugin_market_repo_urls,
)
from app.helper.message import MessageHelper
from app.helper.progress import ProgressHelper
from app.helper.rule import RuleHelper
@@ -70,13 +76,6 @@ _PUBLIC_SYSTEM_CONFIG_KEYS = {
_PUBLIC_SETTINGS_KEYS = {"PLUGIN_MARKET"}
_LOG_DOWNLOAD_LIMIT = 10
_LOG_DOWNLOAD_NAME_PATTERN = re.compile(r"^[A-Za-z0-9_-]+$")
_PLUGIN_MARKET_WIKI_START = "<!-- plugin-market-repos:start -->"
_PLUGIN_MARKET_WIKI_END = "<!-- plugin-market-repos:end -->"
_PLUGIN_MARKET_WIKI_URL = "https://raw.githubusercontent.com/jxxghp/MoviePilot-Wiki/main/plugin.md"
_PLUGIN_MARKET_REPO_PATTERN = re.compile(
r"https?://github\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:\.git)?/?",
re.IGNORECASE,
)
def _validate_llm_server_tool_config(env: dict) -> Optional[str]:
@@ -120,25 +119,6 @@ def _validate_llm_server_tool_config(env: dict) -> Optional[str]:
)
def _normalize_plugin_market_repo_url(repo_url: str) -> Optional[str]:
"""
规范化插件仓库地址,便于跨来源合并去重。
"""
repo_url = (repo_url or "").strip().rstrip("/")
if not repo_url:
return None
repo_url = repo_url.removesuffix(".git")
parsed_url = urlparse(repo_url)
if parsed_url.scheme not in {"http", "https"}:
return None
if (parsed_url.hostname or "").lower() != "github.com":
return None
paths = [item for item in parsed_url.path.split("/") if item]
if len(paths) < 2:
return None
return f"https://github.com/{paths[0]}/{paths[1]}"
def _is_allowed_plugin_market_wiki_url(wiki_url: str) -> bool:
"""
校验插件市场 Wiki 地址是否属于固定文档源。
@@ -156,55 +136,6 @@ def _is_allowed_plugin_market_wiki_url(wiki_url: str) -> bool:
)
def _split_plugin_market_repo_urls(value: Optional[str]) -> list[str]:
"""
拆分插件市场仓库配置并保持原有顺序去重。
"""
repos: list[str] = []
seen_repos = set()
for item in re.split(r"[\n,]+", value or ""):
normalized_repo = _normalize_plugin_market_repo_url(item)
if not normalized_repo or normalized_repo.lower() in seen_repos:
continue
repos.append(normalized_repo)
seen_repos.add(normalized_repo.lower())
return repos
def _extract_plugin_market_repos_from_wiki(markdown: str) -> list[str]:
"""
从 Wiki 插件文档中提取插件仓库地址。
"""
content = markdown or ""
if _PLUGIN_MARKET_WIKI_START in content and _PLUGIN_MARKET_WIKI_END in content:
content = content.split(_PLUGIN_MARKET_WIKI_START, 1)[1].split(_PLUGIN_MARKET_WIKI_END, 1)[0]
repos: list[str] = []
seen_repos = set()
for item in _PLUGIN_MARKET_REPO_PATTERN.findall(content):
normalized_repo = _normalize_plugin_market_repo_url(item)
if not normalized_repo or normalized_repo.lower() in seen_repos:
continue
repos.append(normalized_repo)
seen_repos.add(normalized_repo.lower())
return repos
def _merge_plugin_market_repos(local_repos: list[str], wiki_repos: list[str]) -> list[str]:
"""
合并本地与 Wiki 插件仓库地址,保留本地顺序并追加 Wiki 新地址。
"""
merged_repos: list[str] = []
seen_repos = set()
for repo in local_repos + wiki_repos:
normalized_repo = _normalize_plugin_market_repo_url(repo)
if not normalized_repo or normalized_repo.lower() in seen_repos:
continue
merged_repos.append(normalized_repo)
seen_repos.add(normalized_repo.lower())
return merged_repos
def _match_nettest_prefix(url: str, prefix: str) -> bool:
"""
判断目标URL是否仍然落在允许的协议、主机、端口和路径前缀内。
@@ -889,7 +820,7 @@ async def sync_plugin_market_from_wiki(
"""
从 Wiki 插件文档同步插件市场仓库地址。
"""
wiki_url = (request.wiki_url if request else None) or _PLUGIN_MARKET_WIKI_URL
wiki_url = (request.wiki_url if request else None) or PLUGIN_MARKET_WIKI_URL
wiki_url = wiki_url.strip()
if not _is_allowed_plugin_market_wiki_url(wiki_url):
return schemas.Response(success=False, message="不支持的 Wiki 同步地址")
@@ -909,14 +840,14 @@ async def sync_plugin_market_from_wiki(
message=f"访问 Wiki 插件仓库清单失败,状态码:{res.status_code}",
)
wiki_repos = _extract_plugin_market_repos_from_wiki(res.text)
wiki_repos = extract_plugin_market_repos_from_wiki(res.text)
if not wiki_repos:
return schemas.Response(success=False, message="未在 Wiki 中识别到插件仓库地址")
local_repos = _split_plugin_market_repo_urls(settings.PLUGIN_MARKET)
local_repos = split_plugin_market_repo_urls(settings.PLUGIN_MARKET)
local_repo_keys = {repo.lower() for repo in local_repos}
added_count = len([repo for repo in wiki_repos if repo.lower() not in local_repo_keys])
merged_repos = _merge_plugin_market_repos(local_repos, wiki_repos)
merged_repos = merge_plugin_market_repos(local_repos, wiki_repos)
merged_value = ",".join(merged_repos)
success, message = settings.update_setting("PLUGIN_MARKET", merged_value)

View File

@@ -430,26 +430,7 @@ class ConfigModel(BaseModel):
# ==================== 插件配置 ====================
# 插件市场仓库地址,多个地址使用,分隔,地址以/结尾
PLUGIN_MARKET: str = (
"https://github.com/jxxghp/MoviePilot-Plugins,"
"https://github.com/thsrite/MoviePilot-Plugins,"
"https://github.com/honue/MoviePilot-Plugins,"
"https://github.com/InfinityPacer/MoviePilot-Plugins,"
"https://github.com/DDSRem-Dev/MoviePilot-Plugins,"
"https://github.com/madrays/MoviePilot-Plugins,"
"https://github.com/justzerock/MoviePilot-Plugins,"
"https://github.com/KoWming/MoviePilot-Plugins,"
"https://github.com/wikrin/MoviePilot-Plugins,"
"https://github.com/HankunYu/MoviePilot-Plugins,"
"https://github.com/baozaodetudou/MoviePilot-Plugins,"
"https://github.com/Aqr-K/MoviePilot-Plugins,"
"https://github.com/hotlcc/MoviePilot-Plugins-Third,"
"https://github.com/gxterry/MoviePilot-Plugins,"
"https://github.com/DzAvril/MoviePilot-Plugins,"
"https://github.com/mrtian2016/MoviePilot-Plugins,"
"https://github.com/Hqyel/MoviePilot-Plugins-Third,"
"https://github.com/xijin285/MoviePilot-Plugins,"
"https://github.com/Seed680/MoviePilot-Plugins,"
"https://github.com/imaliang/MoviePilot-Plugins"
"https://github.com/jxxghp/MoviePilot-Plugins"
)
# 插件安装数据共享
PLUGIN_STATISTIC_SHARE: bool = True

98
app/helper/market.py Normal file
View File

@@ -0,0 +1,98 @@
import re
from typing import Optional
from urllib.parse import urlparse
PLUGIN_MARKET_WIKI_START = "<!-- plugin-market-repos:start -->"
PLUGIN_MARKET_WIKI_END = "<!-- plugin-market-repos:end -->"
PLUGIN_MARKET_WIKI_URL = (
"https://raw.githubusercontent.com/jxxghp/MoviePilot-Wiki/main/plugin.md"
)
PLUGIN_MARKET_REPO_PATTERN = re.compile(
r"https?://github\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:\.git)?/?",
re.IGNORECASE,
)
def normalize_plugin_market_repo_url(repo_url: str) -> Optional[str]:
"""
规范化插件仓库地址,便于跨来源合并去重。
"""
repo_url = (repo_url or "").strip().rstrip("/")
if not repo_url:
return None
repo_url = repo_url.removesuffix(".git")
parsed_url = urlparse(repo_url)
if parsed_url.scheme not in {"http", "https"}:
return None
if (parsed_url.hostname or "").lower() != "github.com":
return None
paths = [item for item in parsed_url.path.split("/") if item]
if len(paths) < 2:
return None
return f"https://github.com/{paths[0]}/{paths[1]}"
def split_plugin_market_repo_urls(value: Optional[str]) -> list[str]:
"""
拆分插件市场仓库配置并保持原有顺序去重。
"""
repos: list[str] = []
seen_repos = set()
for item in re.split(r"[\n,]+", value or ""):
normalized_repo = normalize_plugin_market_repo_url(item)
if not normalized_repo or normalized_repo.lower() in seen_repos:
continue
repos.append(normalized_repo)
seen_repos.add(normalized_repo.lower())
return repos
def extract_plugin_market_repos_from_wiki(
markdown: str, require_markers: bool = False
) -> list[str]:
"""
从 Wiki 插件文档中提取插件仓库地址。
:param markdown: Wiki 插件文档 Markdown 内容
:param require_markers: 是否要求文档包含唯一且有序的清单边界标记
:return: 规范化并按文档顺序去重的插件仓库地址
"""
content = markdown or ""
start_count = content.count(PLUGIN_MARKET_WIKI_START)
end_count = content.count(PLUGIN_MARKET_WIKI_END)
start_index = content.find(PLUGIN_MARKET_WIKI_START)
end_index = content.find(PLUGIN_MARKET_WIKI_END)
if start_count == 1 and end_count == 1 and start_index < end_index:
content = content[
start_index + len(PLUGIN_MARKET_WIKI_START):end_index
]
elif require_markers:
raise ValueError("Wiki 插件仓库清单必须包含唯一且有序的开始和结束标记")
repos: list[str] = []
seen_repos = set()
for item in PLUGIN_MARKET_REPO_PATTERN.findall(content):
normalized_repo = normalize_plugin_market_repo_url(item)
if not normalized_repo or normalized_repo.lower() in seen_repos:
continue
repos.append(normalized_repo)
seen_repos.add(normalized_repo.lower())
return repos
def merge_plugin_market_repos(
local_repos: list[str], wiki_repos: list[str]
) -> list[str]:
"""
合并本地与 Wiki 插件仓库地址,保留本地顺序并追加 Wiki 新地址。
"""
merged_repos: list[str] = []
seen_repos = set()
for repo in local_repos + wiki_repos:
normalized_repo = normalize_plugin_market_repo_url(repo)
if not normalized_repo or normalized_repo.lower() in seen_repos:
continue
merged_repos.append(normalized_repo)
seen_repos.add(normalized_repo.lower())
return merged_repos

View File

@@ -110,6 +110,25 @@ chmod +x scripts/start-local.sh
如果资源文件没有放到 `app/helper/`,站点索引、规则和内置资源相关能力可能无法按本地开发预期工作;如果插件没有放到 `app/plugins/`,主程序也不会在本地运行时发现该插件。
### 4.1 GitHub 发版时生成插件市场默认值
源码分支中的 `ConfigModel.PLUGIN_MARKET` 只保留官方插件仓库作为离线兜底。GitHub 的正式版与 Beta 镜像构建会检出 `MoviePilot-Wiki` 的 `main` 分支,并由 `scripts/generate_plugin_market_default.py` 读取 `plugin.md` 中 `plugin-market-repos:start/end` 标记区域,将规范化、去重后的公开仓库清单写入构建工作区。
生成过程遵循以下约束:
- 标记必须唯一、顺序正确,清单不能为空且必须包含 `jxxghp/MoviePilot-Plugins`;不满足时直接终止构建。
- 生成脚本只替换 `ConfigModel` 中的 `PLUGIN_MARKET` 默认值,不写入运行时环境变量,因此用户仍可通过系统环境变量或 `/config/app.env` 覆盖。
- 正式版工作流会创建仅由 Release Tag 引用的本地快照提交Docker 镜像和 Tag 源码归档均来自该快照Actions 不会将生成结果回写到 `v2` 分支。
- Release Tag 快照提交信息和镜像标签会记录本次使用的 MoviePilot Wiki Commit便于追溯清单来源。
本地验证生成结果时,先激活项目虚拟环境,再执行:
```bash
python -m scripts.generate_plugin_market_default \
--wiki-file /path/to/MoviePilot-Wiki/plugin.md \
--config-file app/core/config.py
```
### 5. 运行安全检查
我们使用 `safety` 工具检查依赖项中是否存在已知安全漏洞。更新运行时依赖后,应至少检查运行时入口;更新开发测试依赖时,也应覆盖开发入口。

View File

@@ -285,4 +285,20 @@ bash scripts/collect-site-adapter.sh
- Never put a Cookie or other credential in command arguments or shell history.
- Feature Request attachments are public. Review all four files in the generated ZIP before attaching it, and never attach raw HTML, HAR, or browser network archives.
*Last Updated: 2026-07-12*
---
## Plugin Market Release Default
```bash
# Run after activating the project virtual environment
python -m scripts.generate_plugin_market_default \
--wiki-file /path/to/MoviePilot-Wiki/plugin.md \
--config-file app/core/config.py
```
**Rules:**
- The Wiki document must contain exactly one `plugin-market-repos:start/end` marker pair.
- The marked list must be nonempty and include `jxxghp/MoviePilot-Plugins`.
- This command rewrites only `ConfigModel.PLUGIN_MARKET`; inspect the resulting diff before committing or packaging.
*Last Updated: 2026-08-06*

View File

@@ -0,0 +1,84 @@
"""
根据 MoviePilot Wiki 清单生成发版快照中的插件市场默认值。
"""
import argparse
import ast
from pathlib import Path
from typing import Optional
from app.helper.market import extract_plugin_market_repos_from_wiki
OFFICIAL_PLUGIN_MARKET = "https://github.com/jxxghp/MoviePilot-Plugins"
def _parse_args(args: Optional[list[str]] = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="生成插件市场发版默认值")
parser.add_argument("--wiki-file", type=Path, required=True)
parser.add_argument("--config-file", type=Path, required=True)
return parser.parse_args(args)
def _find_plugin_market_assignment(source: str) -> tuple[int, int, str]:
tree = ast.parse(source)
source_lines = source.splitlines(keepends=True)
for node in tree.body:
if not isinstance(node, ast.ClassDef) or node.name != "ConfigModel":
continue
for item in node.body:
if not isinstance(item, ast.AnnAssign):
continue
if not isinstance(item.target, ast.Name):
continue
if item.target.id != "PLUGIN_MARKET" or item.end_lineno is None:
continue
start = item.lineno - 1
indent_size = len(source_lines[start]) - len(source_lines[start].lstrip())
indent = source_lines[start][:indent_size]
return start, item.end_lineno, indent
raise ValueError("未在 ConfigModel 中找到 PLUGIN_MARKET 默认值")
def _format_plugin_market_assignment(repos: list[str], indent: str) -> str:
lines = [f"{indent}PLUGIN_MARKET: str = (\n"]
for index, repo in enumerate(repos):
suffix = "," if index < len(repos) - 1 else ""
lines.append(f'{indent} "{repo}{suffix}"\n')
lines.append(f"{indent})\n")
return "".join(lines)
def _generate_plugin_market_default(wiki_file: Path, config_file: Path) -> list[str]:
markdown = wiki_file.read_text(encoding="utf-8")
repos = extract_plugin_market_repos_from_wiki(markdown, require_markers=True)
if not repos:
raise ValueError("Wiki 插件仓库清单为空")
if OFFICIAL_PLUGIN_MARKET not in repos:
raise ValueError("Wiki 插件仓库清单缺少 MoviePilot 官方插件仓库")
source = config_file.read_text(encoding="utf-8")
start, end, indent = _find_plugin_market_assignment(source)
source_lines = source.splitlines(keepends=True)
replacement = _format_plugin_market_assignment(repos, indent)
updated_source = (
"".join(source_lines[:start])
+ replacement
+ "".join(source_lines[end:])
)
config_file.write_text(updated_source, encoding="utf-8")
return repos
def main(args: Optional[list[str]] = None) -> int:
"""
读取 Wiki 清单并更新指定配置文件中的插件市场默认值。
"""
options = _parse_args(args)
repos = _generate_plugin_market_default(options.wiki_file, options.config_file)
print(f"已生成 PLUGIN_MARKET 默认值,共 {len(repos)} 个仓库")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,129 @@
import ast
from pathlib import Path
import pytest
from app.helper.market import extract_plugin_market_repos_from_wiki
from scripts.generate_plugin_market_default import (
OFFICIAL_PLUGIN_MARKET,
_generate_plugin_market_default,
)
def _read_plugin_market_default(config_file: Path) -> str:
tree = ast.parse(config_file.read_text(encoding="utf-8"))
for node in tree.body:
if not isinstance(node, ast.ClassDef) or node.name != "ConfigModel":
continue
for item in node.body:
if not isinstance(item, ast.AnnAssign):
continue
if isinstance(item.target, ast.Name) and item.target.id == "PLUGIN_MARKET":
return ast.literal_eval(item.value)
raise AssertionError("未找到 PLUGIN_MARKET 默认值")
def test_extract_plugin_market_repos_uses_marked_section_and_deduplicates() -> None:
"""
Wiki 清单解析只读取标记区域,并规范化、去重仓库地址。
"""
markdown = """
- https://github.com/outside/ignored
<!-- plugin-market-repos:start -->
- https://github.com/jxxghp/MoviePilot-Plugins/
- https://github.com/demo/Market.git
- https://github.com/demo/Market
<!-- plugin-market-repos:end -->
- https://github.com/outside/ignored-again
"""
assert extract_plugin_market_repos_from_wiki(
markdown, require_markers=True
) == [
OFFICIAL_PLUGIN_MARKET,
"https://github.com/demo/Market",
]
def test_extract_plugin_market_repos_requires_unique_markers_for_build() -> None:
"""
构建模式拒绝缺失或重复边界标记的 Wiki 文档。
"""
with pytest.raises(ValueError, match="唯一且有序的开始和结束标记"):
extract_plugin_market_repos_from_wiki(
f"- {OFFICIAL_PLUGIN_MARKET}", require_markers=True
)
markdown = f"""
<!-- plugin-market-repos:start -->
<!-- plugin-market-repos:start -->
- {OFFICIAL_PLUGIN_MARKET}
<!-- plugin-market-repos:end -->
"""
with pytest.raises(ValueError, match="唯一且有序的开始和结束标记"):
extract_plugin_market_repos_from_wiki(markdown, require_markers=True)
def test_generate_plugin_market_default_updates_assignment_idempotently(
tmp_path: Path,
) -> None:
"""
生成脚本只替换 ConfigModel 默认值,并保持重复执行结果一致。
"""
wiki_file = tmp_path / "plugin.md"
wiki_file.write_text(
f"""
<!-- plugin-market-repos:start -->
- {OFFICIAL_PLUGIN_MARKET}
- https://github.com/demo/MoviePilot-Plugins
<!-- plugin-market-repos:end -->
""",
encoding="utf-8",
)
config_file = tmp_path / "config.py"
config_file.write_text(
"""class ConfigModel(BaseModel):
PLUGIN_MARKET: str = "https://github.com/old/Market"
OTHER_SETTING: bool = True
""",
encoding="utf-8",
)
repos = _generate_plugin_market_default(wiki_file, config_file)
first_result = config_file.read_text(encoding="utf-8")
_generate_plugin_market_default(wiki_file, config_file)
assert repos == [
OFFICIAL_PLUGIN_MARKET,
"https://github.com/demo/MoviePilot-Plugins",
]
assert _read_plugin_market_default(config_file) == ",".join(repos)
assert "OTHER_SETTING: bool = True" in first_result
assert config_file.read_text(encoding="utf-8") == first_result
def test_generate_plugin_market_default_requires_official_repo(
tmp_path: Path,
) -> None:
"""
发版默认清单缺少官方仓库时终止生成。
"""
wiki_file = tmp_path / "plugin.md"
wiki_file.write_text(
"""
<!-- plugin-market-repos:start -->
- https://github.com/demo/MoviePilot-Plugins
<!-- plugin-market-repos:end -->
""",
encoding="utf-8",
)
config_file = tmp_path / "config.py"
config_file.write_text(
"""class ConfigModel(BaseModel):
PLUGIN_MARKET: str = "https://github.com/old/Market"
""",
encoding="utf-8",
)
with pytest.raises(ValueError, match="缺少 MoviePilot 官方插件仓库"):
_generate_plugin_market_default(wiki_file, config_file)