Skip to main content

glancetest: add a function to convert a video file to an MP3

ID
bdb4a5d
date
2026-09-12 19:55:42+00:00
author
Alex Chan <alex@alexwlchan.net>
parent
b17f22e
message
glancetest: add a function to convert a video file to an MP3
changed files
3 files, 74 additions, 1 deletion

Changed files

glancecast.py (518 → 1363)

diff --git a/glancecast.py b/glancecast.py
index 50c76c3..7306709 100644
--- a/glancecast.py
+++ b/glancecast.py
@@ -6,8 +6,11 @@ This allows you to listen to a video in an audio-only podcast player like
 Overcast, and glance at the screen if you need some visual context.
 """
 
+from pathlib import Path
+import subprocess
 import shutil
 import sys
+import tempfile
 
 
 def ensure_tool_installed(name: str):
@@ -18,3 +21,31 @@ def ensure_tool_installed(name: str):
     p = shutil.which(name)
     if p is None:
         sys.exit(f"missing required tool: {name}")
+
+
+def ffmpeg(*args: str | Path) -> None:
+    """
+    Run an ffmpeg command and ensure it succeeds.
+    """
+    ensure_tool_installed("ffmpeg")
+    cmd = ["ffmpeg"] + [str(a) for a in args]
+
+    # TODO: This dumps the ffmpeg output to stderr. Reduce it or suppress
+    # it unless the command fails.
+    subprocess.check_call(cmd)
+
+
+def convert_video_to_mp3(video_path: Path) -> Path:
+    """
+    Convert a video file to an MP3 file. Return the path to the new MP3 file.
+    """
+    if not video_path.exists():
+        raise FileNotFoundError(f"no video file found: {video_path}")
+
+    tmp_dir = Path(tempfile.mkdtemp())
+    mp3_path = tmp_dir / (video_path.stem + ".mp3")
+
+    # Example: `ffmpeg -i video.mp4 audio.mp3`
+    ffmpeg("-i", video_path, mp3_path)
+
+    return Path(mp3_path)

test_glancecast.py (622 → 1757)

diff --git a/test_glancecast.py b/test_glancecast.py
index ed85144..e7328d7 100644
--- a/test_glancecast.py
+++ b/test_glancecast.py
@@ -2,9 +2,13 @@
 Tests for glancecast.
 """
 
+from pathlib import Path
+from subprocess import CalledProcessError
+
+from mutagen.mp3 import MP3
 import pytest
 
-from glancecast import ensure_tool_installed
+from glancecast import convert_video_to_mp3, ensure_tool_installed
 
 
 class TestEnsureToolInstalled:
@@ -24,3 +28,41 @@ class TestEnsureToolInstalled:
         """
         with pytest.raises(SystemExit, match="missing required tool"):
             ensure_tool_installed("does_not_exist")
+
+
+class TestConvertVideoToMP3:
+    """
+    Tests for `convert_video_to_mp3`.
+    """
+
+    def test_creates_mp3_file(self) -> None:
+        """
+        Convert a video file to an MP3, and check it's the expected length.
+        """
+        p = Path("tests/fixtures/lego_age_picker.mp4")
+
+        mp3_path = convert_video_to_mp3(p)
+        assert mp3_path.exists()
+        assert mp3_path.suffix == ".mp3"
+
+        mp3 = MP3(mp3_path)
+        assert mp3.info is not None
+        assert mp3.info.length == 19.536
+
+    def test_missing_video_file(self) -> None:
+        """
+        Trying to convert a non-existent video file is an error.
+        """
+        p = Path("does_not_exist.mkv")
+
+        with pytest.raises(FileNotFoundError):
+            convert_video_to_mp3(p)
+
+    def test_convert_non_video_file(self) -> None:
+        """
+        Trying to convert a non-video file is an error.
+        """
+        p = Path("README.md")
+
+        with pytest.raises(CalledProcessError):
+            convert_video_to_mp3(p)

tests/fixtures/lego_age_picker.mp4 (0 → 916048)

diff --git a/tests/fixtures/lego_age_picker.mp4 b/tests/fixtures/lego_age_picker.mp4
new file mode 100644
index 0000000..93f0606
Binary files /dev/null and b/tests/fixtures/lego_age_picker.mp4 differ