aboutsummaryrefslogtreecommitdiff
path: root/tests/test_audio.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_audio.py')
-rw-r--r--tests/test_audio.py132
1 files changed, 130 insertions, 2 deletions
diff --git a/tests/test_audio.py b/tests/test_audio.py
index 29bf4ea..f36ed54 100644
--- a/tests/test_audio.py
+++ b/tests/test_audio.py
@@ -1,11 +1,21 @@
-"""Tests for audio helpers: speed parameters, chunk cleanup, and encoding."""
+"""Tests for audio helpers: speed parameters, chunk cleanup, encoding,
+command construction, and duration verification."""
import tempfile
import unittest
from pathlib import Path
+from converter import audio
from converter import config
-from converter.audio import _encode_args, build_ffmetadata, cleanup_chunks, speed_export_params
+from converter.audio import (
+ _encode_args,
+ build_concat_command,
+ build_ffmetadata,
+ build_m4b_chapters_command,
+ cleanup_chunks,
+ speed_export_params,
+ verify_output_duration,
+)
class SpeedExportParamsTests(unittest.TestCase):
@@ -76,6 +86,124 @@ class EncodeArgsTests(unittest.TestCase):
def test_m4b_uses_aac(self):
self.assertEqual(_encode_args("m4b"), ["-c:a", "aac", "-b:a", config.AUDIO_BITRATE])
+ def test_wav_is_lossless_pcm(self):
+ self.assertEqual(_encode_args("wav"), ["-c:a", "pcm_s16le"])
+
+
+class M4bContainerArgsTests(unittest.TestCase):
+ def setUp(self):
+ self._original = audio._brand_supported
+ audio._brand_supported = True
+
+ def tearDown(self):
+ audio._brand_supported = self._original
+
+ def test_includes_faststart_and_brand(self):
+ args = audio._m4b_container_args()
+ self.assertIn("+faststart", args)
+ self.assertIn("M4B ", args)
+
+ def test_brand_omitted_when_unsupported(self):
+ audio._brand_supported = False
+ self.assertEqual(audio._m4b_container_args(), ["-movflags", "+faststart"])
+
+
+class BuildConcatCommandTests(unittest.TestCase):
+ def setUp(self):
+ self._original = audio._brand_supported
+ audio._brand_supported = True
+
+ def tearDown(self):
+ audio._brand_supported = self._original
+
+ def test_mp3_has_no_container_flags(self):
+ cmd = build_concat_command(Path("list.txt"), Path("out.mp3"), "mp3")
+ self.assertEqual(cmd[:6], ["ffmpeg", "-y", "-f", "concat", "-safe", "0"])
+ self.assertNotIn("-movflags", cmd)
+ self.assertEqual(cmd[-1], "out.mp3")
+
+ def test_m4b_gets_faststart_and_brand(self):
+ cmd = build_concat_command(Path("list.txt"), Path("out.m4b"), "m4b")
+ self.assertIn("+faststart", cmd)
+ self.assertIn("M4B ", cmd)
+ self.assertEqual(cmd[-1], "out.m4b")
+
+ def test_speed_copy_writes_two_outputs(self):
+ cmd = build_concat_command(Path("list.txt"), Path("out.m4b"), "m4b",
+ speed=1.5, speed_path=Path("out_1.5.m4b"))
+ self.assertIn("out.m4b", cmd)
+ self.assertIn("out_1.5.m4b", cmd)
+ # faststart must apply to both outputs
+ self.assertEqual(cmd.count("+faststart"), 2)
+ self.assertTrue(any("atempo=1.5" in arg for arg in cmd))
+
+ def test_wav_intermediate(self):
+ cmd = build_concat_command(Path("list.txt"), Path("chapter.wav"), "wav")
+ self.assertIn("pcm_s16le", cmd)
+ self.assertNotIn("-movflags", cmd)
+
+
+class BuildM4bChaptersCommandTests(unittest.TestCase):
+ def setUp(self):
+ self._original = audio._brand_supported
+ audio._brand_supported = True
+
+ def tearDown(self):
+ audio._brand_supported = self._original
+
+ def test_base_output_maps_metadata_and_chapters(self):
+ cmd = build_m4b_chapters_command(Path("list.txt"), Path("meta.txt"), Path("out.m4b"))
+ self.assertIn("-map_metadata", cmd)
+ self.assertIn("-map_chapters", cmd)
+ self.assertIn("out.m4b", cmd)
+ self.assertIn("+faststart", cmd)
+ self.assertNotIn("filter_complex", cmd)
+
+ def test_speed_outputs_get_their_own_chapter_metadata(self):
+ cmd = build_m4b_chapters_command(
+ Path("list.txt"), Path("meta.txt"), Path("out.m4b"),
+ speed=2.0, speed_path=Path("out_2.m4b"), speed_metadata_file=Path("meta2.txt"),
+ )
+ self.assertEqual(cmd.count("+faststart"), 2)
+ self.assertEqual(cmd.count("-map_chapters"), 2)
+ self.assertTrue(any("atempo=2" in arg for arg in cmd))
+ # base output chapters come from metadata input 1, speed copy from 2
+ chapter_flags = [i for i, v in enumerate(cmd) if v == "-map_chapters"]
+ self.assertEqual(cmd[chapter_flags[0] + 1], "1")
+ self.assertEqual(cmd[chapter_flags[1] + 1], "2")
+ base_idx, speed_idx = cmd.index("out.m4b"), cmd.index("out_2.m4b")
+ self.assertLess(chapter_flags[0], base_idx)
+ self.assertGreater(chapter_flags[1], base_idx)
+ self.assertLess(chapter_flags[1], speed_idx)
+
+
+class VerifyOutputDurationTests(unittest.TestCase):
+ def _patch_probe(self, ms):
+ audio.probe_duration_ms = lambda path: ms
+
+ def setUp(self):
+ self._original_probe = audio.probe_duration_ms
+
+ def tearDown(self):
+ audio.probe_duration_ms = self._original_probe
+
+ def test_close_duration_passes(self):
+ self._patch_probe(100_000)
+ self.assertTrue(verify_output_duration(Path("x.m4b"), 101_000))
+
+ def test_unverifiable_duration_passes(self):
+ self._patch_probe(0)
+ self.assertTrue(verify_output_duration(Path("x.m4b"), 100_000))
+
+ def test_zero_expected_passes(self):
+ self._patch_probe(50_000)
+ self.assertTrue(verify_output_duration(Path("x.m4b"), 0))
+
+ def test_large_drift_fails(self):
+ self._patch_probe(3_600_000) # bogus "1 hour" for a 1 minute book
+ with self.assertLogs(level="ERROR"):
+ self.assertFalse(verify_output_duration(Path("x.m4b"), 60_000))
+
class BuildFFMetadataTests(unittest.TestCase):
def test_writes_chapters(self):