# -*- coding: utf-8 -*- """media_assets 共享媒体资源解析测试。""" from __future__ import annotations import json import platform import zipfile from pathlib import Path from unittest.mock import patch import pytest from jiangchang_skill_core import media_assets as ma def _make_complete_assets(root: Path, *, with_ffmpeg: bool = True, with_ffprobe: bool = True) -> None: (root / "music" / "calm").mkdir(parents=True) (root / "fonts").mkdir(parents=True) (root / "watermark").mkdir(parents=True) bin_key = ma._platform_bin_key() bin_dir = root / "bin" / bin_key bin_dir.mkdir(parents=True) if with_ffmpeg: (bin_dir / ma._tool_filename("ffmpeg")).write_bytes(b"ffmpeg") if with_ffprobe: (bin_dir / ma._tool_filename("ffprobe")).write_bytes(b"ffprobe") def _sample_manifest() -> dict: bin_key = ma._platform_bin_key() return { "name": "media-assets", "version": "0.2.0", "directories": {"music": "music", "fonts": "fonts", "watermark": "watermark"}, "tools": { "ffmpeg": { bin_key: { "package": "ffmpeg-release-essentials", "archive_type": "zip", "urls": [{"name": "test", "url": "https://example.test/ffmpeg.zip"}], "archive_paths": { "ffmpeg": "*/bin/ffmpeg.exe", "ffprobe": "*/bin/ffprobe.exe", }, } } }, } def _install_tools_from_manifest(root: Path, warnings: list[str]) -> bool: bin_key = ma._platform_bin_key() bin_dir = root / "bin" / bin_key bin_dir.mkdir(parents=True, exist_ok=True) (bin_dir / ma._tool_filename("ffmpeg")).write_bytes(b"ffmpeg") (bin_dir / ma._tool_filename("ffprobe")).write_bytes(b"ffprobe") return True def test_media_assets_root_env_priority(tmp_path: Path) -> None: custom = tmp_path / "custom-media" custom.mkdir() env = { "MEDIA_ASSETS_ROOT": str(custom), "JIANGCHANG_DATA_ROOT": str(tmp_path / "data"), } assert ma.resolve_media_assets_root(env=env) == custom def test_default_media_assets_path(tmp_path: Path) -> None: env = {"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")} expected = tmp_path / "data" / "shared" / "media-assets" assert ma.resolve_media_assets_root(env=env) == expected def test_complete_assets_skips_download(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: root = tmp_path / "data" / "shared" / "media-assets" _make_complete_assets(root) def _fail_download(*args, **kwargs): raise AssertionError("download should not be called") monkeypatch.setattr(ma, "_try_download_via_zip", _fail_download) monkeypatch.setattr(ma, "_try_download_via_git", _fail_download) status = ma.ensure_media_assets(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")}) assert status.ready is True assert status.warnings == [] assert status.source == "local" def test_missing_ffmpeg_warning(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: root = tmp_path / "data" / "shared" / "media-assets" _make_complete_assets(root, with_ffmpeg=False) monkeypatch.setattr(ma, "_try_download_via_zip", lambda *a, **k: False) monkeypatch.setattr(ma, "_try_download_via_git", lambda *a, **k: False) status = ma.ensure_media_assets(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")}) assert status.ready is False assert "ffmpeg_missing" in status.warnings def test_missing_ffprobe_warning(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: root = tmp_path / "data" / "shared" / "media-assets" _make_complete_assets(root, with_ffprobe=False) monkeypatch.setattr(ma, "_try_download_via_zip", lambda *a, **k: False) monkeypatch.setattr(ma, "_try_download_via_git", lambda *a, **k: False) status = ma.ensure_media_assets(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")}) assert status.ready is False assert "ffprobe_missing" in status.warnings def test_pick_background_music_stable_first(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: root = tmp_path / "data" / "shared" / "media-assets" _make_complete_assets(root) music = root / "music" (music / "b-upbeat.mp3").write_bytes(b"mp3") (music / "calm" / "a-calm.mp3").write_bytes(b"mp3") (music / "z-last.wav").write_bytes(b"wav") monkeypatch.setattr( ma, "ensure_media_assets", lambda *a, **k: ma._inspect_media_assets(root, source="local"), ) picked = ma.pick_background_music(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")}) 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/.keep", "") 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" shared = data_root / "shared" target = shared / "media-assets" archive = tmp_path / "media-assets.zip" _build_archive_zip(archive, "media-assets-main") def _fake_download(url: str, dest: Path) -> None: 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) status = ma.ensure_media_assets(env={"JIANGCHANG_DATA_ROOT": str(data_root)}) assert target.is_dir() assert status.ready is True assert status.source == "zip" assert "media_assets_zip_invalid_layout" not in status.warnings def test_zip_download_failure_returns_warning(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: import urllib.error def _raise_url_error(url: str, dest: Path) -> None: raise urllib.error.URLError("network down") monkeypatch.setattr(ma, "_download_zip", _raise_url_error) monkeypatch.setattr(ma, "_find_git_executable", lambda: None) status = ma.ensure_media_assets(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")}) assert status.ready is False assert "media_assets_zip_download_failed" in status.warnings assert "git_not_available" in status.warnings def test_git_not_available_when_zip_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: import urllib.error monkeypatch.setattr( ma, "_download_zip", lambda url, dest: (_ for _ in ()).throw(urllib.error.URLError("fail")), ) monkeypatch.setattr(ma, "_find_git_executable", lambda: None) status = ma.ensure_media_assets(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")}) assert "git_not_available" in status.warnings def test_media_assets_root_override_no_download(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: custom = tmp_path / "override" custom.mkdir() def _fail_download(*args, **kwargs): raise AssertionError("download should not be called when MEDIA_ASSETS_ROOT is set") monkeypatch.setattr(ma, "_try_download_via_zip", _fail_download) status = ma.ensure_media_assets(env={"MEDIA_ASSETS_ROOT": str(custom)}) assert status.root == custom assert status.exists is True @pytest.mark.skipif(platform.system().lower() != "windows", reason="Windows default path check") def test_windows_default_data_root() -> None: with patch.dict("os.environ", {}, clear=True): root = ma.resolve_media_assets_root() assert root == Path(r"D:\jiangchang-data") / "shared" / "media-assets" def test_manifest_ffmpeg_download(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: root = tmp_path / "data" / "shared" / "media-assets" (root / "music").mkdir(parents=True) (root / "fonts").mkdir(parents=True) (root / "watermark").mkdir(parents=True) (root / "manifest.json").write_text( json.dumps(_sample_manifest(), ensure_ascii=False), encoding="utf-8", ) monkeypatch.setattr(ma, "_try_download_via_zip", lambda *a, **k: False) monkeypatch.setattr(ma, "_try_download_via_git", lambda *a, **k: False) monkeypatch.setattr(ma, "_try_download_ffmpeg_tools", _install_tools_from_manifest) status = ma.ensure_media_assets(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")}) assert status.ready is True assert status.ffmpeg_path is not None assert status.ffprobe_path is not None