From e59661f2d8293debcde6106855987af4762dac64 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Sat, 6 Jun 2026 16:16:18 +0800 Subject: [PATCH] fix(media-assets): download stable release bundle Co-authored-by: Cursor --- README.md | 9 ++- src/jiangchang_skill_core/media_assets.py | 34 +++++++-- tests/test_media_assets.py | 90 ++++++++++++++++++++--- 3 files changed, 109 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 492e45c..5e547fe 100644 --- a/README.md +++ b/README.md @@ -86,15 +86,16 @@ send_prompt_via_composer(page, "第一行\n第二行") 读取背景音乐、字体、水印和 ffmpeg 工具。 -如果目录不存在,会优先通过 ZIP 下载公开仓库: +如果目录不存在,会优先下载稳定 release bundle(不依赖用户电脑安装 Git): -`https://git.jc2009.com/client-commons/media-assets/archive/main.zip` +`https://git.jc2009.com/client-commons/media-assets/releases/download/vlatest/media-assets.zip` -这不依赖用户电脑安装 Git。 +bundle 内含背景音乐、字体与水印;**不包含** ffmpeg 二进制。ffmpeg / ffprobe 仍由 bundle 内 `manifest.json` 的 `tools.ffmpeg` 下载源单独拉取。 可通过环境变量覆盖: -`MEDIA_ASSETS_ROOT=/path/to/custom/media-assets` +- `MEDIA_ASSETS_ROOT=/path/to/custom/media-assets` — 本地资源目录 +- `MEDIA_ASSETS_BUNDLE_URL=https://example.com/media-assets.zip` — bundle 下载地址 ### 录屏合成(`screencast`) diff --git a/src/jiangchang_skill_core/media_assets.py b/src/jiangchang_skill_core/media_assets.py index 8b11e3c..acaa1e4 100644 --- a/src/jiangchang_skill_core/media_assets.py +++ b/src/jiangchang_skill_core/media_assets.py @@ -18,9 +18,10 @@ from datetime import datetime from pathlib import Path from typing import Optional -MEDIA_ASSETS_ZIP_URL = ( - "https://git.jc2009.com/client-commons/media-assets/archive/main.zip" +MEDIA_ASSETS_BUNDLE_URL = ( + "https://git.jc2009.com/client-commons/media-assets/releases/download/vlatest/media-assets.zip" ) +MEDIA_ASSETS_ZIP_URL = MEDIA_ASSETS_BUNDLE_URL MEDIA_ASSETS_GIT_URL = "https://git.jc2009.com/client-commons/media-assets" _WIN_DEFAULT_DATA_ROOT = Path(r"D:\jiangchang-data") @@ -109,6 +110,12 @@ def _env_dict(env: dict[str, str] | None) -> dict[str, str]: return env if env is not None else dict(os.environ) +def _media_assets_bundle_url(env: dict[str, str] | None = None) -> str: + env_map = _env_dict(env) + override = (env_map.get("MEDIA_ASSETS_BUNDLE_URL") or "").strip() + return override or MEDIA_ASSETS_BUNDLE_URL + + def _resolve_jiangchang_data_root( data_root: str | Path | None = None, env: dict[str, str] | None = None, @@ -474,12 +481,16 @@ def _try_download_via_zip( shared: Path, target: Path, warnings: list[str], + *, + env: dict[str, str] | None = None, + url: str | None = None, ) -> bool: download_root = _download_dir(shared) download_root.mkdir(parents=True, exist_ok=True) zip_path = download_root / "media-assets.zip" extracted = download_root / "extracted" staging = download_root / "staging" + bundle_url = url or _media_assets_bundle_url(env) try: if staging.exists(): @@ -487,7 +498,7 @@ def _try_download_via_zip( if extracted.exists(): shutil.rmtree(extracted, ignore_errors=True) - _download_zip(MEDIA_ASSETS_ZIP_URL, zip_path) + _download_zip(bundle_url, zip_path) _extract_zip(zip_path, extracted) found = _find_media_assets_root_in_tree(extracted) @@ -565,10 +576,15 @@ def _needs_media_repo_content(status: MediaAssetsStatus) -> bool: ) -def _maybe_repair_music_content(root: Path, status: MediaAssetsStatus) -> MediaAssetsStatus: +def _maybe_repair_music_content( + root: Path, + status: MediaAssetsStatus, + *, + env: dict[str, str] | None = None, +) -> MediaAssetsStatus: if not _needs_music_content_repair(status): return status - if _media_assets_root_overridden(None): + if _media_assets_root_overridden(env): return status warnings: list[str] = [] @@ -581,7 +597,7 @@ def _maybe_repair_music_content(root: Path, status: MediaAssetsStatus) -> MediaA return _maybe_ensure_ffmpeg_tools(root, refreshed) shared = _shared_parent(root) - if _try_download_via_zip(shared, root, warnings): + if _try_download_via_zip(shared, root, warnings, env=env): refreshed = _inspect_media_assets(root, source="zip") for warning in warnings: if warning not in refreshed.warnings: @@ -631,12 +647,13 @@ def ensure_media_assets( shared.mkdir(parents=True, exist_ok=True) warnings: list[str] = [] - if _try_download_via_zip(shared, root, warnings): + if _try_download_via_zip(shared, root, warnings, env=env_map): return _maybe_ensure_ffmpeg_tools( root, _maybe_repair_music_content( root, _inspect_media_assets(root, source="zip"), + env=env_map, ), ) @@ -650,6 +667,7 @@ def ensure_media_assets( root, source="git" if _is_git_repo(root) else "local", ), + env=env_map, ), ) @@ -657,7 +675,7 @@ def ensure_media_assets( for warning in warnings: if warning not in final.warnings: final.warnings.append(warning) - repaired = _maybe_repair_music_content(root, final) + repaired = _maybe_repair_music_content(root, final, env=env_map) for warning in warnings: if warning not in repaired.warnings: repaired.warnings.append(warning) diff --git a/tests/test_media_assets.py b/tests/test_media_assets.py index b8e1fb7..c4d1842 100644 --- a/tests/test_media_assets.py +++ b/tests/test_media_assets.py @@ -58,6 +58,56 @@ def _install_tools_from_manifest(root: Path, warnings: list[str]) -> bool: return True +def _build_archive_zip(dest: Path, inner_name: str) -> None: + manifest = _sample_manifest() + with zipfile.ZipFile(dest, "w") as zf: + zf.writestr(f"{inner_name}/README.md", "# media-assets\n") + zf.writestr(f"{inner_name}/manifest.json", json.dumps(manifest, ensure_ascii=False)) + zf.writestr(f"{inner_name}/music/calm/track.mp3", b"x" * 256) + zf.writestr(f"{inner_name}/fonts/.keep", "") + zf.writestr(f"{inner_name}/watermark/.keep", "") + + +def test_default_bundle_url_is_release() -> None: + url = ma._media_assets_bundle_url() + assert url == ma.MEDIA_ASSETS_BUNDLE_URL + assert url.endswith("/releases/download/vlatest/media-assets.zip") + assert "/archive/main.zip" not in url + + +def test_bundle_url_env_override() -> None: + override = "https://example.com/media-assets.zip" + assert ma._media_assets_bundle_url(env={"MEDIA_ASSETS_BUNDLE_URL": override}) == override + + +def test_zip_download_uses_bundle_url_override(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + data_root = tmp_path / "data" + shared = data_root / "shared" + target = shared / "media-assets" + archive = tmp_path / "media-assets.zip" + override = "https://example.com/media-assets.zip" + + _build_archive_zip(archive, "media-assets") + captured_urls: list[str] = [] + + def _fake_download(url: str, dest: Path) -> None: + captured_urls.append(url) + dest.write_bytes(archive.read_bytes()) + + monkeypatch.setattr(ma, "_download_zip", _fake_download) + monkeypatch.setattr(ma, "_find_git_executable", lambda: None) + monkeypatch.setattr(ma, "_try_download_ffmpeg_tools", _install_tools_from_manifest) + + env = { + "JIANGCHANG_DATA_ROOT": str(data_root), + "MEDIA_ASSETS_BUNDLE_URL": override, + } + status = ma.ensure_media_assets(env=env) + assert captured_urls == [override] + assert target.is_dir() + assert status.ready is True + + def test_media_assets_root_env_priority(tmp_path: Path) -> None: custom = tmp_path / "custom-media" custom.mkdir() @@ -132,23 +182,18 @@ def test_pick_background_music_stable_first(tmp_path: Path, monkeypatch: pytest. assert picked == music / "b-upbeat.mp3" -def _build_archive_zip(dest: Path, inner_name: str) -> None: - manifest = _sample_manifest() - with zipfile.ZipFile(dest, "w") as zf: - zf.writestr(f"{inner_name}/README.md", "# media-assets\n") - zf.writestr(f"{inner_name}/manifest.json", json.dumps(manifest, ensure_ascii=False)) - zf.writestr(f"{inner_name}/music/calm/track.mp3", b"x" * 256) - zf.writestr(f"{inner_name}/fonts/.keep", "") - zf.writestr(f"{inner_name}/watermark/.keep", "") - - -def test_zip_wrapper_layout_detected(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +@pytest.mark.parametrize("inner_name", ["media-assets", "media-assets-main"]) +def test_zip_wrapper_layout_detected( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + inner_name: str, +) -> None: data_root = tmp_path / "data" shared = data_root / "shared" target = shared / "media-assets" archive = tmp_path / "media-assets.zip" - _build_archive_zip(archive, "media-assets-main") + _build_archive_zip(archive, inner_name) def _fake_download(url: str, dest: Path) -> None: dest.write_bytes(archive.read_bytes()) @@ -226,6 +271,27 @@ def test_probe_media_assets_does_not_download(tmp_path: Path, monkeypatch: pytes assert status.ffmpeg_path is None +def test_lfs_pointer_warning_in_probe_and_ensure(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + root = tmp_path / "data" / "shared" / "media-assets" + _make_complete_assets(root) + music = root / "music" + (music / "calm" / "track.mp3").unlink() + (music / "lfs-only.mp3").write_text( + "version https://git-lfs.github.com/spec/v1\noid sha256:abc\nsize 123\n" + ) + + monkeypatch.setattr(ma, "_try_download_via_zip", lambda *a, **k: False) + monkeypatch.setattr(ma, "_try_download_via_git", lambda *a, **k: False) + + probed = ma.probe_media_assets(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")}) + assert "background_music_lfs_pointer_only" in probed.warnings + assert probed.music_ready is False + + ensured = ma.ensure_media_assets(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")}) + assert "background_music_lfs_pointer_only" in ensured.warnings + assert ensured.ready is False + + def test_pick_background_music_skips_lfs_pointer(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: root = tmp_path / "data" / "shared" / "media-assets" _make_complete_assets(root, with_ffmpeg=True, with_ffprobe=True)