diff --git a/scenedetect/_cli/__init__.py b/scenedetect/_cli/__init__.py index ac7e0976..2111171b 100644 --- a/scenedetect/_cli/__init__.py +++ b/scenedetect/_cli/__init__.py @@ -346,7 +346,7 @@ def scenedetect( ) @click.pass_context def help_command(ctx: click.Context, command_name: str): - """Print help for command (`help [command]`).""" + """Print full help reference.""" assert isinstance(ctx.parent.command, click.MultiCommand) parent_command = ctx.parent.command all_commands = set(parent_command.list_commands(ctx)) @@ -1442,30 +1442,52 @@ def save_images_command( ctx.save_images = True +@click.command("export-qp", cls=_Command) +@click.option( + "--filename", + "-f", + metavar="NAME", + default=None, + type=click.STRING, + help="Filename format to use. %s" % (USER_CONFIG.get_help_string("export-qp", "filename")), +) +@click.pass_context +def export_qp_command( + ctx: click.Context, + filename: ty.Optional[ty.AnyStr], +): + ctx = ctx.obj + assert isinstance(ctx, CliContext) + + export_qp_args = { + "filename_format": ctx.config.get_value("save-images", "filename", filename), + } + ctx.add_command(cli_commands.export_qp, export_qp_args) + + # ---------------------------------------------------------------------- -# Commands Omitted From Help List +# CLI Sub-Command Registration # ---------------------------------------------------------------------- -# Info Commands +# Informational scenedetect.add_command(about_command) scenedetect.add_command(help_command) scenedetect.add_command(version_command) -# ---------------------------------------------------------------------- -# Commands Added To Help List -# ---------------------------------------------------------------------- - -# Input / Output -scenedetect.add_command(export_html_command) -scenedetect.add_command(list_scenes_command) +# Input scenedetect.add_command(load_scenes_command) -scenedetect.add_command(save_images_command) -scenedetect.add_command(split_video_command) scenedetect.add_command(time_command) -# Detection Algorithms +# Detectors scenedetect.add_command(detect_adaptive_command) scenedetect.add_command(detect_content_command) scenedetect.add_command(detect_hash_command) scenedetect.add_command(detect_hist_command) scenedetect.add_command(detect_threshold_command) + +# Output +scenedetect.add_command(export_html_command) +scenedetect.add_command(export_qp_command) +scenedetect.add_command(list_scenes_command) +scenedetect.add_command(save_images_command) +scenedetect.add_command(split_video_command) diff --git a/scenedetect/_cli/commands.py b/scenedetect/_cli/commands.py index 83a23bf2..91ff5f26 100644 --- a/scenedetect/_cli/commands.py +++ b/scenedetect/_cli/commands.py @@ -64,6 +64,27 @@ def export_html( ) +def export_qp( + context: CliContext, + scenes: SceneList, + cuts: CutList, + filename_format: str, +): + """Handler for the `export-qp` command.""" + del scenes # We only use cuts for this handler. + + qp_path = Template(filename_format).safe_substitute(VIDEO_NAME=context.video_stream.name) + with open(qp_path, "w") as qp_file: + # TODO(#388): Instead of setting start time, should we always start at 0 and shift each + # cut by the amount that was seeked? + first_frame = 0 if context.start_time is None else context.start_time.frame_num + # Place an initial I frame at the first frame. + qp_file.writelines([f"{first_frame} I -1"]) + # Place another I frame at each detected cut. + qp_file.writelines(f"{cut.frame_num} I -1" for cut in cuts) + logger.info(f"QP file written to: {qp_path}") + + def list_scenes( context: CliContext, scenes: SceneList, diff --git a/scenedetect/_cli/config.py b/scenedetect/_cli/config.py index 5e03e5d2..7a179aff 100644 --- a/scenedetect/_cli/config.py +++ b/scenedetect/_cli/config.py @@ -301,6 +301,9 @@ def format(self, timecode: FrameTimecode) -> str: "image-width": 0, "no-images": False, }, + "export-qp": { + "filename": "$VIDEO_NAME.qp", + }, "list-scenes": { "cut-format": "timecode", "display-cuts": True, diff --git a/tests/test_cli.py b/tests/test_cli.py index dbd9ef90..3c40cb08 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -431,6 +431,19 @@ def test_cli_export_html(tmp_path: Path): # TODO: Check for existence of HTML & image files. +def test_cli_export_qp(tmp_path: Path): + """Test `export-qp` command.""" + base_command = "-i {VIDEO} -s {STATS} time {TIME} {DETECTOR} {COMMAND}" + assert invoke_scenedetect(base_command, COMMAND="export-qp", output_dir=tmp_path) == 0 + assert ( + invoke_scenedetect( + base_command, COMMAND="export-qp --filename custom.txt", output_dir=tmp_path + ) + == 0 + ) + # TODO: Check for existence of QP files. + + @pytest.mark.parametrize("backend_type", ALL_BACKENDS) def test_cli_backend(backend_type: str): """Test setting the `-b`/`--backend` argument."""