media,static_site_tests: disallow VP9-encoded videos as well as AV1
- ID
d8bb257- date
2026-08-26 21:18:10+00:00- author
Alex Chan <alex@alexwlchan.net>- parent
e5e2f88- message
media,static_site_tests: disallow VP9-encoded videos as well as AV1- changed files
15 files, 95 additions, 55 deletionsCHANGELOG.mdjavascript/chives-image.jsjavascript/chives-pagination.jsjavascript/chives-sorting.jsjavascript/chives-video.jspyproject.tomlpython/chives/__init__.pypython/chives/cli.pypython/chives/javascript.pypython/chives/media.pypython/chives/static_site_tests.pytests/fixtures/media/Big_Buck_Bunny_360_10s_1MB.webmtests/python/test_media.pytests/python/test_static_site_tests.pytests/test_versions.py
Changed files
CHANGELOG.md (7294 → 7690)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3323906..150ac9e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# CHANGELOG
+## v53 - 2026-08-26
+
+Static site tests now disallow videos encoded in VP9, in addition to the previously-disallowed AV1.
+
+The latest version of yt-dlp downloads YouTube videos in VP9, which I can't play on my Mac.
+
+To support this change, the `media.is_av1_video` function has been replaced with `media.get_video_codec`.
+This is exposed as a Python API and through a `get_video_codec` CLI tool.
+
## v52 - 2026-07-20
Static site tests now allow keeping a Python `requirements.in` and `requirements.txt` file in the top-level of a static site folder.
javascript/chives-image.js (6561 → 6561)
diff --git a/javascript/chives-image.js b/javascript/chives-image.js
index ec63606..020efa3 100644
--- a/javascript/chives-image.js
+++ b/javascript/chives-image.js
@@ -1,5 +1,5 @@
// javascript/chives-image.js
-// chives version: 52
+// chives version: 53
// ChivesImage provides a <chives-image> component that renders
// an ImageEntity on a web page.
javascript/chives-pagination.js (4929 → 4929)
diff --git a/javascript/chives-pagination.js b/javascript/chives-pagination.js
index 34b4f78..583b229 100644
--- a/javascript/chives-pagination.js
+++ b/javascript/chives-pagination.js
@@ -1,5 +1,5 @@
// javascript/chives-pagination.js
-// chives version: 52
+// chives version: 53
// ChivesPagination manages the pagination of a collection of items.
//
javascript/chives-sorting.js (5034 → 5034)
diff --git a/javascript/chives-sorting.js b/javascript/chives-sorting.js
index 42c8208..343c6f5 100644
--- a/javascript/chives-sorting.js
+++ b/javascript/chives-sorting.js
@@ -1,5 +1,5 @@
// javascript/chives-sorting.js
-// chives version: 52
+// chives version: 53
// ChivesSorting manages the sorting of a collection of items.
//
javascript/chives-video.js (5147 → 5147)
diff --git a/javascript/chives-video.js b/javascript/chives-video.js
index eaf2363..81f7773 100644
--- a/javascript/chives-video.js
+++ b/javascript/chives-video.js
@@ -1,5 +1,5 @@
// javascript/chives-video.js
-// chives version: 52
+// chives version: 53
// ChivesVideo provides a <chives-video> component that renders
// a VideoEntity on a web page.
pyproject.toml (1958 → 2003)
diff --git a/pyproject.toml b/pyproject.toml
index 0d8b540..5b98774 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -42,6 +42,7 @@ urls = ["certifi"]
chives-list-js = "chives.cli:list_chives_js"
chives-update-js = "chives.cli:update_js_components_cli"
chives-write-js = "chives.cli:write_chives_js"
+get_codec = "chives.cli:get_video_codec_cli"
srt-to-vtt = "chives.cli:convert_srt_to_vtt_cli"
[project.urls]
python/chives/__init__.py (391 → 391)
diff --git a/python/chives/__init__.py b/python/chives/__init__.py
index da3d17d..ade97f4 100644
--- a/python/chives/__init__.py
+++ b/python/chives/__init__.py
@@ -11,4 +11,4 @@ I share across multiple sites.
"""
-__version__ = "52"
+__version__ = "53"
python/chives/cli.py (2188 → 2452)
diff --git a/python/chives/cli.py b/python/chives/cli.py
index 4f730ca..c2cf322 100644
--- a/python/chives/cli.py
+++ b/python/chives/cli.py
@@ -8,7 +8,7 @@ from pathlib import Path
import sys
from chives.javascript import update_js_components
-from chives.media import convert_srt_to_vtt
+from chives.media import convert_srt_to_vtt, get_video_codec
def convert_srt_to_vtt_cli() -> None: # pragma: no cover
@@ -68,3 +68,15 @@ def write_chives_js() -> None: # pragma: no cover
print(filename)
else:
sys.exit(f"unrecognised JS file: {filename!r}")
+
+
+def get_video_codec_cli() -> None: # pragma: no cover
+ """
+ Print the codec of a video file.
+ """
+ try:
+ path = sys.argv[1]
+ except IndexError:
+ sys.exit(f"Usage: {__file__} PATH")
+
+ print(get_video_codec(path))
python/chives/javascript.py (2388 → 2354)
diff --git a/python/chives/javascript.py b/python/chives/javascript.py
index a79c552..ae4218a 100644
--- a/python/chives/javascript.py
+++ b/python/chives/javascript.py
@@ -2,7 +2,6 @@
Helper functions for working with the JavaScript components.
"""
-import glob
from importlib.resources.abc import Traversable
import os
from pathlib import Path
@@ -46,7 +45,7 @@ def update_js_components(root: Path, js_assets: Traversable | Path) -> None:
"""
print(f"Scanning for chives JavaScript components in: {root}")
- target_files = glob.glob("**/chives-*.js", recursive=True, root_dir=root)
+ target_files = list(root.rglob("chives-*.js"))
if not target_files:
print("No 'chives-*.js' files found in the current directory.")
@@ -69,7 +68,7 @@ def update_js_components(root: Path, js_assets: Traversable | Path) -> None:
except ValueError:
pass
- print(f"Updating {coloured(filepath, 'blue')}")
+ print(f"Updating {coloured(str(filepath), 'blue')}")
Path(root / filepath).write_text(internal_file.read_text())
updated_count += 1
python/chives/media.py (11912 → 11899)
diff --git a/python/chives/media.py b/python/chives/media.py
index 8a58113..d6a4283 100644
--- a/python/chives/media.py
+++ b/python/chives/media.py
@@ -30,16 +30,16 @@ __all__ = [
"create_video_entity",
"get_media_paths",
"get_tint_colour",
- "is_av1_video",
+ "get_video_codec",
"ImageEntity",
"MediaEntity",
"VideoEntity",
]
-def is_av1_video(path: str | Path) -> bool:
+def get_video_codec(path: str | Path) -> str:
"""
- Return True if a video is encoded with AV1, False otherwise.
+ Return a video's codec. For example h264, av1, vp9.
"""
# fmt: off
cmd = [
@@ -64,7 +64,7 @@ def is_av1_video(path: str | Path) -> bool:
output = subprocess.check_output(cmd, text=True)
- return output.strip() == "av1"
+ return output.strip()
class ImageEntity(TypedDict):
python/chives/static_site_tests.py (8947 → 8973)
diff --git a/python/chives/static_site_tests.py b/python/chives/static_site_tests.py
index a921cce..dc553cc 100644
--- a/python/chives/static_site_tests.py
+++ b/python/chives/static_site_tests.py
@@ -7,7 +7,6 @@ import collections
from collections.abc import Iterator
import concurrent.futures
from difflib import SequenceMatcher
-import glob
import itertools
import os
from pathlib import Path
@@ -19,7 +18,7 @@ import pytest
from chives import __version__, javascript
from chives.dates import date_matches_any_format, find_all_dates
-from chives.media import is_av1_video
+from chives.media import get_video_codec
from chives.urls import is_url_safe
@@ -180,28 +179,32 @@ class StaticSiteTestSuite[M](ABC):
assert bad_paths == set(), f"Found paths which aren't URL safe: {bad_paths}"
@pytest.mark.skipif("SKIP_AV1" in os.environ, reason="skip slow test")
- def test_no_videos_are_av1(self, site_root: Path) -> None:
+ def test_videos_use_allowed_codecs(self, site_root: Path) -> None:
"""
- No videos are encoded in AV1 (which doesn't play on my iPhone).
-
- This test can be removed when I upgrade all my devices to ones with
- hardware AV1 decoding support.
-
- See https://alexwlchan.net/2025/av1-on-my-iphone/
+ Videos only use codecs that play on my devices.
"""
- av1_videos = set()
+ bad_videos = set()
+ disallowed_codecs = {
+ "av1", # AV1 doesn't play on my iPhone
+ "vp9", # VP9 doesn't play on my Mac mini
+ }
+
+ affected_paths = [
+ p for p in site_root.rglob("*") if p.suffix in {".mp4", ".webm"}
+ ]
with concurrent.futures.ThreadPoolExecutor() as executor:
futures = {
- executor.submit(is_av1_video, site_root / p): p
- for p in glob.glob("**/*.mp4", root_dir=site_root, recursive=True)
+ executor.submit(get_video_codec, site_root / p): p
+ for p in affected_paths
}
-
concurrent.futures.wait(futures)
- av1_videos = {p for fut, p in futures.items() if fut.result()}
+ bad_videos = {
+ p for fut, p in futures.items() if fut.result() in disallowed_codecs
+ }
- assert av1_videos == set(), f"Found videos encoded with AV1: {av1_videos}"
+ assert bad_videos == set(), f"Found videos with disallowed codecs: {bad_videos}"
def test_no_subtitles_are_srt(self, site_root: Path) -> None:
"""
@@ -210,7 +213,7 @@ class StaticSiteTestSuite[M](ABC):
All subtitles should be encoded as VTT.
"""
- srt_subtitles = set(glob.glob("**/*.srt", root_dir=site_root, recursive=True))
+ srt_subtitles = set(site_root.rglob("*.srt"))
assert srt_subtitles == set(), (
f"Found subtitles encoded as SRT: {srt_subtitles}"
@@ -268,7 +271,7 @@ class StaticSiteTestSuite[M](ABC):
"""
bad_versions = {
p
- for p in glob.glob(f"{site_root}/**/chives-*.js", recursive=True)
+ for p in site_root.rglob("chives-*.js")
if javascript.get_version(p) != __version__
}
tests/fixtures/media/Big_Buck_Bunny_360_10s_1MB.webm (0 → 1052940)
diff --git a/tests/fixtures/media/Big_Buck_Bunny_360_10s_1MB.webm b/tests/fixtures/media/Big_Buck_Bunny_360_10s_1MB.webm
new file mode 100644
index 0000000..76f6275
Binary files /dev/null and b/tests/fixtures/media/Big_Buck_Bunny_360_10s_1MB.webm differ
tests/python/test_media.py (18739 → 19019)
diff --git a/tests/python/test_media.py b/tests/python/test_media.py
index 88b6e9c..ed9c9b0 100644
--- a/tests/python/test_media.py
+++ b/tests/python/test_media.py
@@ -11,7 +11,7 @@ from chives.media import (
create_image_entity,
create_video_entity,
get_media_paths,
- is_av1_video,
+ get_video_codec,
)
@@ -23,13 +23,24 @@ def fixtures_dir() -> Path:
return Path("tests/fixtures/media")
-def test_is_av1_video(fixtures_dir: Path) -> None:
- """is_av1_video correctly detects AV1 videos."""
- # These two videos were downloaded from
- # https://test-videos.co.uk/sintel/mp4-h264 and
- # https://test-videos.co.uk/sintel/mp4-av1
- assert not is_av1_video(fixtures_dir / "Sintel_360_10s_1MB_H264.mp4")
- assert is_av1_video(fixtures_dir / "Sintel_360_10s_1MB_AV1.mp4")
+@pytest.mark.parametrize(
+ "name, codec",
+ [
+ # These two videos were downloaded from
+ # https://test-videos.co.uk/sintel/mp4-h264 and
+ # https://test-videos.co.uk/sintel/mp4-av1
+ ("Sintel_360_10s_1MB_H264.mp4", "h264"),
+ ("Sintel_360_10s_1MB_AV1.mp4", "av1"),
+ # This video was downloaded from
+ # https://test-videos.co.uk/bigbuckbunny/webm-vp9
+ ("Big_Buck_Bunny_360_10s_1MB.webm", "vp9"),
+ ],
+)
+def test_get_video_codec(fixtures_dir: Path, name: str, codec: str) -> None:
+ """
+ get_video_codec correctly detects a video's codec.
+ """
+ assert get_video_codec(fixtures_dir / name) == codec
class TestCreateImageEntity:
tests/python/test_static_site_tests.py (12144 → 12230)
diff --git a/tests/python/test_static_site_tests.py b/tests/python/test_static_site_tests.py
index 62597ac..42a6f8b 100644
--- a/tests/python/test_static_site_tests.py
+++ b/tests/python/test_static_site_tests.py
@@ -173,30 +173,35 @@ def test_checks_for_url_safe_paths(pytester: Pytester, site_root: Path) -> None:
pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
-def test_checks_for_av1_videos(pytester: Pytester, site_root: Path) -> None:
+@pytest.mark.parametrize(
+ "name, is_allowed_codec",
+ [
+ ("Sintel_360_10s_1MB_H264.mp4", True),
+ ("Sintel_360_10s_1MB_AV1.mp4", False),
+ ("Big_Buck_Bunny_360_10s_1MB.webm", False),
+ ],
+)
+def test_checks_video_encoding(
+ pytester: Pytester, site_root: Path, name: str, is_allowed_codec: bool
+) -> None:
"""
- The tests check for AV1-encoded videos.
+ The tests check for videos with the wrong encoding.
"""
create_pyfile(pytester, site_root)
- keyword = "test_no_videos_are_av1"
+ keyword = "test_videos_use_allowed_codecs"
# This should pass trivially when the site is empty.
pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
- # Copy in an H.264-encoded video, and check it's not flagged.
- shutil.copyfile(
- GIT_ROOT / "tests/fixtures/media/Sintel_360_10s_1MB_H264.mp4",
- site_root / "Sintel_360_10s_1MB_H264.mp4",
- )
- pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
+ # Copy in an H.264-encoded video, and check it's flagged or not
+ # depending on whether the codec is allowed.
+ shutil.copyfile(GIT_ROOT / "tests/fixtures/media" / name, site_root / name)
- # Copy in an AV1-encoded video, and check it's caught by the test
- shutil.copyfile(
- GIT_ROOT / "tests/fixtures/media/Sintel_360_10s_1MB_AV1.mp4",
- site_root / "Sintel_360_10s_1MB_AV1.mp4",
- )
- pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
+ if is_allowed_codec:
+ pytester.runpytest("-k", keyword).assert_outcomes(passed=1)
+ else:
+ pytester.runpytest("-k", keyword).assert_outcomes(failed=1)
def test_checks_for_srt_subtitles(pytester: Pytester, site_root: Path) -> None:
tests/test_versions.py (491 → 507)
diff --git a/tests/test_versions.py b/tests/test_versions.py
index bb19771..f78438e 100644
--- a/tests/test_versions.py
+++ b/tests/test_versions.py
@@ -2,7 +2,7 @@
Check that the JavaScript files and Python versions are in sync.
"""
-import glob
+from pathlib import Path
from chives import __version__
from chives.javascript import get_version
@@ -12,7 +12,7 @@ def test_versions_in_sync() -> None:
"""
The JavaScript components have the same version as the Python module.
"""
- for filepath in glob.glob("javascript/*.js"):
+ for filepath in Path("javascript").glob("*.js"):
print(filepath)
assert get_version(filepath) == __version__, (
f"want {__version__}, got {get_version(filepath)}"