diff --git a/scenedetect/output/__init__.py b/scenedetect/output/__init__.py index 6fa26585..7e5fac31 100644 --- a/scenedetect/output/__init__.py +++ b/scenedetect/output/__init__.py @@ -69,7 +69,7 @@ def write_scene_list( - output_csv_file: ty.TextIO, + output_csv_file: ty.TextIO | str | Path, scene_list: SceneList, include_cut_list: bool = True, cut_list: CutList | None = None, @@ -79,7 +79,8 @@ def write_scene_list( """Writes the given list of scenes to an output file handle in CSV format. Arguments: - output_csv_file: Handle to open file in write mode. + output_csv_file: Handle to open file in write mode, or a filesystem path. A path is + opened with a context manager so the file is closed after writing. scene_list: List of pairs of FrameTimecodes denoting each scene's start/end FrameTimecode. include_cut_list: Bool indicating if the first row should include the timecodes where each scene starts. Should be set to False if RFC 4180 compliant CSV output is required. @@ -92,6 +93,17 @@ def write_scene_list( Raises: TypeError: "delimiter" must be a 1-character string """ + if isinstance(output_csv_file, (str, Path)): + with open(output_csv_file, "w", newline="") as file_handle: + write_scene_list( + file_handle, + scene_list, + include_cut_list=include_cut_list, + cut_list=cut_list, + col_separator=col_separator, + row_separator=row_separator, + ) + return csv_writer = csv.writer(output_csv_file, delimiter=col_separator, lineterminator=row_separator) # If required, output the cutting list as the first row (i.e. before the header row). if include_cut_list: diff --git a/tests/test_output.py b/tests/test_output.py index 8d1b4d9f..1f434873 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -13,6 +13,7 @@ import json from fractions import Fraction +from io import StringIO from pathlib import Path from xml.etree import ElementTree @@ -31,6 +32,7 @@ VideoMetadata, is_ffmpeg_available, split_video_ffmpeg, + write_scene_list, write_scene_list_edl, write_scene_list_fcp7, write_scene_list_fcpx, @@ -248,6 +250,34 @@ def _fake_scenes(fps: Fraction, frames): return [(FrameTimecode(start, fps=fps), FrameTimecode(end, fps=fps)) for start, end in frames] +def test_write_scene_list_file_handle(): + """Existing callers that pass an open file handle keep working.""" + scenes = _fake_scenes(_FPS_CFR, [(0, 30), (30, 60)]) + buf = StringIO() + write_scene_list(buf, scenes, include_cut_list=False) + text = buf.getvalue() + assert "Scene Number" in text + assert "00:00:00.000" in text or "00:00:00:00" in text + + +def test_write_scene_list_accepts_str_path(tmp_path: Path): + """A filesystem path string must not raise TypeError from csv.writer.""" + scenes = _fake_scenes(_FPS_CFR, [(0, 30)]) + output_path = tmp_path / "scenes.csv" + write_scene_list(str(output_path), scenes, include_cut_list=False) + assert output_path.exists() + assert "Scene Number" in output_path.read_text() + + +def test_write_scene_list_accepts_path(tmp_path: Path): + """pathlib.Path is opened with a context manager and closed after write.""" + scenes = _fake_scenes(_FPS_CFR, [(0, 30)]) + output_path = tmp_path / "scenes.csv" + write_scene_list(output_path, scenes, include_cut_list=False) + assert output_path.exists() + assert "Scene Number" in output_path.read_text() + + def test_write_scene_list_edl(tmp_path: Path): """EDL output has title header, FCM line, and one event per scene in CMX 3600 format.""" scenes = _fake_scenes(_FPS_CFR, [(0, 30), (30, 60)])