fix(media-assets): download stable release bundle

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-06 16:16:18 +08:00
parent 434a405e19
commit e59661f2d8
3 changed files with 109 additions and 24 deletions

View File

@@ -86,15 +86,16 @@ send_prompt_via_composer(page, "第一行\n第二行")
读取背景音乐、字体、水印和 ffmpeg 工具。 读取背景音乐、字体、水印和 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` ### 录屏合成(`screencast`

View File

@@ -18,9 +18,10 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
MEDIA_ASSETS_ZIP_URL = ( MEDIA_ASSETS_BUNDLE_URL = (
"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"
) )
MEDIA_ASSETS_ZIP_URL = MEDIA_ASSETS_BUNDLE_URL
MEDIA_ASSETS_GIT_URL = "https://git.jc2009.com/client-commons/media-assets" MEDIA_ASSETS_GIT_URL = "https://git.jc2009.com/client-commons/media-assets"
_WIN_DEFAULT_DATA_ROOT = Path(r"D:\jiangchang-data") _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) 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( def _resolve_jiangchang_data_root(
data_root: str | Path | None = None, data_root: str | Path | None = None,
env: dict[str, str] | None = None, env: dict[str, str] | None = None,
@@ -474,12 +481,16 @@ def _try_download_via_zip(
shared: Path, shared: Path,
target: Path, target: Path,
warnings: list[str], warnings: list[str],
*,
env: dict[str, str] | None = None,
url: str | None = None,
) -> bool: ) -> bool:
download_root = _download_dir(shared) download_root = _download_dir(shared)
download_root.mkdir(parents=True, exist_ok=True) download_root.mkdir(parents=True, exist_ok=True)
zip_path = download_root / "media-assets.zip" zip_path = download_root / "media-assets.zip"
extracted = download_root / "extracted" extracted = download_root / "extracted"
staging = download_root / "staging" staging = download_root / "staging"
bundle_url = url or _media_assets_bundle_url(env)
try: try:
if staging.exists(): if staging.exists():
@@ -487,7 +498,7 @@ def _try_download_via_zip(
if extracted.exists(): if extracted.exists():
shutil.rmtree(extracted, ignore_errors=True) 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) _extract_zip(zip_path, extracted)
found = _find_media_assets_root_in_tree(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): if not _needs_music_content_repair(status):
return status return status
if _media_assets_root_overridden(None): if _media_assets_root_overridden(env):
return status return status
warnings: list[str] = [] warnings: list[str] = []
@@ -581,7 +597,7 @@ def _maybe_repair_music_content(root: Path, status: MediaAssetsStatus) -> MediaA
return _maybe_ensure_ffmpeg_tools(root, refreshed) return _maybe_ensure_ffmpeg_tools(root, refreshed)
shared = _shared_parent(root) 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") refreshed = _inspect_media_assets(root, source="zip")
for warning in warnings: for warning in warnings:
if warning not in refreshed.warnings: if warning not in refreshed.warnings:
@@ -631,12 +647,13 @@ def ensure_media_assets(
shared.mkdir(parents=True, exist_ok=True) shared.mkdir(parents=True, exist_ok=True)
warnings: list[str] = [] 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( return _maybe_ensure_ffmpeg_tools(
root, root,
_maybe_repair_music_content( _maybe_repair_music_content(
root, root,
_inspect_media_assets(root, source="zip"), _inspect_media_assets(root, source="zip"),
env=env_map,
), ),
) )
@@ -650,6 +667,7 @@ def ensure_media_assets(
root, root,
source="git" if _is_git_repo(root) else "local", source="git" if _is_git_repo(root) else "local",
), ),
env=env_map,
), ),
) )
@@ -657,7 +675,7 @@ def ensure_media_assets(
for warning in warnings: for warning in warnings:
if warning not in final.warnings: if warning not in final.warnings:
final.warnings.append(warning) 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: for warning in warnings:
if warning not in repaired.warnings: if warning not in repaired.warnings:
repaired.warnings.append(warning) repaired.warnings.append(warning)

View File

@@ -58,6 +58,56 @@ def _install_tools_from_manifest(root: Path, warnings: list[str]) -> bool:
return True 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: def test_media_assets_root_env_priority(tmp_path: Path) -> None:
custom = tmp_path / "custom-media" custom = tmp_path / "custom-media"
custom.mkdir() custom.mkdir()
@@ -132,23 +182,18 @@ def test_pick_background_music_stable_first(tmp_path: Path, monkeypatch: pytest.
assert picked == music / "b-upbeat.mp3" assert picked == music / "b-upbeat.mp3"
def _build_archive_zip(dest: Path, inner_name: str) -> None: @pytest.mark.parametrize("inner_name", ["media-assets", "media-assets-main"])
manifest = _sample_manifest() def test_zip_wrapper_layout_detected(
with zipfile.ZipFile(dest, "w") as zf: tmp_path: Path,
zf.writestr(f"{inner_name}/README.md", "# media-assets\n") monkeypatch: pytest.MonkeyPatch,
zf.writestr(f"{inner_name}/manifest.json", json.dumps(manifest, ensure_ascii=False)) inner_name: str,
zf.writestr(f"{inner_name}/music/calm/track.mp3", b"x" * 256) ) -> None:
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:
data_root = tmp_path / "data" data_root = tmp_path / "data"
shared = data_root / "shared" shared = data_root / "shared"
target = shared / "media-assets" target = shared / "media-assets"
archive = tmp_path / "media-assets.zip" 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: def _fake_download(url: str, dest: Path) -> None:
dest.write_bytes(archive.read_bytes()) 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 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: def test_pick_background_music_skips_lfs_pointer(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
root = tmp_path / "data" / "shared" / "media-assets" root = tmp_path / "data" / "shared" / "media-assets"
_make_complete_assets(root, with_ffmpeg=True, with_ffprobe=True) _make_complete_assets(root, with_ffmpeg=True, with_ffprobe=True)