Skip to main content

media: fix a bug with byte order marks in SRT files

ID
76d0c27
date
2026-07-16 20:13:51+00:00
author
Alex Chan <alex@alexwlchan.net>
parent
037db4b
message
media: fix a bug with byte order marks in SRT files
changed files
4 files, 58 additions, 1 deletion

Changed files

CHANGELOG.md (5618) → CHANGELOG.md (5957)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index be20b6a..9726b45 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
 # CHANGELOG
 
+## v45 - 2026-07-16
+
+Fix a bug with SRT subtitle files that contain a [byte order mark (BOM)][wiki-bom].
+Specifically, when converting those files to VTT, the caption number at the start of the SRT would be written to the VTT, and the BOM in the VTT would be in the wrong place.
+
+[wiki-bom]: https://en.wikipedia.org/wiki/Byte_order_mark
+
 ## v44 - 2026-07-13
 
 Improve support for VTT subtitles (which work with the HTML5-native `<video>` element) rather than SRT:

src/chives/__init__.py (391) → src/chives/__init__.py (391)

diff --git a/src/chives/__init__.py b/src/chives/__init__.py
index 3ad17cb..1ac2f82 100644
--- a/src/chives/__init__.py
+++ b/src/chives/__init__.py
@@ -11,4 +11,4 @@ I share across multiple sites.
 
 """
 
-__version__ = "44"
+__version__ = "45"

src/chives/media.py (11785) → src/chives/media.py (12113)

diff --git a/src/chives/media.py b/src/chives/media.py
index 1fcc4a3..015ae55 100644
--- a/src/chives/media.py
+++ b/src/chives/media.py
@@ -430,10 +430,19 @@ def convert_srt_to_vtt(srt_path: Path) -> Path:
     with open(srt_path) as srt_file:
         srt_lines = srt_file.readlines()
 
+    if len(srt_lines) == 0:
+        raise ValueError("cannot convert empty SRT file")
+
     vtt_path = srt_path.with_suffix(".vtt")
     assert vtt_path != srt_path, vtt_path
 
     with open(vtt_path, "w") as vtt_file:
+        # Check for a byte order mark in the first line; if so, preserve
+        # it in the converted VTT file.
+        if srt_lines[0].startswith("\ufeff"):
+            vtt_file.write("\ufeff")
+            srt_lines[0] = srt_lines[0][1:]
+
         # Write the required WEBVTT header and blank line
         vtt_file.write("WEBVTT\n\n")
 

tests/test_media.py (16303) → tests/test_media.py (17704)

diff --git a/tests/test_media.py b/tests/test_media.py
index 1a73457..5109adb 100644
--- a/tests/test_media.py
+++ b/tests/test_media.py
@@ -470,6 +470,47 @@ class TestConvertSrtToVtt:
             "Yet another person said the third thing!"
         )
 
+    def test_converting_srt_with_bom(self, tmp_path: Path) -> None:
+        """
+        If an SRT file starts with \ufeff (a byte order mark), it's
+        correctly removed from the caption number.
+        """
+        srt_path = tmp_path / "captions_with_bom.en.srt"
+        srt_path.write_text(
+            "\ufeff1\n"
+            "00:00:01,001 --> 00:00:10,010\n"
+            "Somebody said the first thing\n"
+            "\n"
+            "2\n"
+            "00:02:00,002 --> 00:20:00,020\n"
+            "Somebody else said the second thing\n"
+        )
+
+        vtt_path = convert_srt_to_vtt(srt_path)
+        assert vtt_path == (tmp_path / "captions_with_bom.en.vtt")
+        assert vtt_path.exists()
+        assert not srt_path.exists()
+
+        assert vtt_path.read_text() == (
+            "\ufeffWEBVTT\n"
+            "\n"
+            "00:00:01.001 --> 00:00:10.010\n"
+            "Somebody said the first thing\n"
+            "\n"
+            "00:02:00.002 --> 00:20:00.020\n"
+            "Somebody else said the second thing\n"
+        )
+
+    def test_convert_empty_file_is_error(self, tmp_path: Path) -> None:
+        """
+        Converting an empty file is an error.
+        """
+        srt_path = tmp_path / "captions_with_bom.en.srt"
+        srt_path.write_text("")
+
+        with pytest.raises(ValueError, match="cannot convert empty SRT file"):
+            convert_srt_to_vtt(srt_path)
+
     def test_converting_non_srt_file_is_error(self) -> None:
         """
         Converting a non-SRT file returns an error.