feat: sync plugin markets from wiki

This commit is contained in:
jxxghp
2026-06-14 12:57:40 +08:00
parent 0f3e9574ab
commit 93713ba662
5 changed files with 266 additions and 62 deletions

View File

@@ -68,6 +68,98 @@ _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 _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 地址是否属于固定文档源。
"""
parsed_url = urlparse(wiki_url)
if parsed_url.scheme != "https":
return False
if (parsed_url.hostname or "").lower() != "raw.githubusercontent.com":
return False
return bool(
re.fullmatch(
r"/jxxghp/MoviePilot-Wiki/[^/]+/plugin\.md",
parsed_url.path,
)
)
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:
@@ -723,6 +815,73 @@ async def get_public_setting(
return schemas.Response(success=True, data={"value": value})
@router.post(
"/setting/PLUGIN_MARKET/sync-wiki",
summary="从Wiki同步插件市场仓库",
response_model=schemas.Response,
)
async def sync_plugin_market_from_wiki(
request: Optional[schemas.PluginMarketSyncRequest] = Body(default=None),
_: User = Depends(get_current_active_superuser_async),
) -> schemas.Response:
"""
从 Wiki 插件文档同步插件市场仓库地址。
"""
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 同步地址")
res = await AsyncRequestUtils(
ua=settings.USER_AGENT,
proxies=settings.PROXY,
timeout=30,
content_type=None,
accept_type="text/plain,*/*",
).get_res(wiki_url)
if res is None:
return schemas.Response(success=False, message="无法访问 Wiki 插件仓库清单")
if res.status_code != 200:
return schemas.Response(
success=False,
message=f"访问 Wiki 插件仓库清单失败,状态码:{res.status_code}",
)
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_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_value = ",".join(merged_repos)
success, message = settings.update_setting("PLUGIN_MARKET", merged_value)
if success:
await eventmanager.async_send_event(
etype=EventType.ConfigChanged,
data=ConfigChangeEventData(
key="PLUGIN_MARKET", value=merged_value, change_type="update"
),
)
elif success is None:
success = True
return schemas.Response(
success=success,
message=message,
data={
"value": merged_value,
"repos": merged_repos,
"wiki_repos": wiki_repos,
"added_count": added_count,
"total_count": len(merged_repos),
"source_url": wiki_url,
},
)
@router.get("/setting/{key}", summary="查询系统设置", response_model=schemas.Response)
async def get_setting(
key: str, _: User = Depends(get_current_active_superuser_async)