Skip to content

Commit 82f5325

Browse files
committed
[cli] Add new export-qp command (#388)
1 parent 43a02d1 commit 82f5325

File tree

5 files changed

+100
-16
lines changed

5 files changed

+100
-16
lines changed

scenedetect.cfg

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,15 @@
283283
#start-col-name = Start Frame
284284

285285

286+
[save-qp]
287+
288+
# Filename format of QP file. Can use $VIDEO_NAME macro.
289+
#filename = $VIDEO_NAME.qp
290+
291+
# Folder to output QP file to. Overrides [global] output option.
292+
#output = /usr/tmp/images
293+
294+
286295
#
287296
# BACKEND OPTIONS
288297
#

scenedetect/_cli/__init__.py

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ def scenedetect(
346346
)
347347
@click.pass_context
348348
def help_command(ctx: click.Context, command_name: str):
349-
"""Print help for command (`help [command]`)."""
349+
"""Print full help reference."""
350350
assert isinstance(ctx.parent.command, click.MultiCommand)
351351
parent_command = ctx.parent.command
352352
all_commands = set(parent_command.list_commands(ctx))
@@ -1011,7 +1011,7 @@ def export_html_command(
10111011
"-o",
10121012
metavar="DIR",
10131013
type=click.Path(exists=False, dir_okay=True, writable=True, resolve_path=False),
1014-
help="Output directory to save videos to. Overrides global option -o/--output if set.%s"
1014+
help="Output directory to save videos to. Overrides global option -o/--output.%s"
10151015
% (USER_CONFIG.get_help_string("list-scenes", "output", show_default=False)),
10161016
)
10171017
@click.option(
@@ -1084,7 +1084,7 @@ def list_scenes_command(
10841084
"-o",
10851085
metavar="DIR",
10861086
type=click.Path(exists=False, dir_okay=True, writable=True, resolve_path=False),
1087-
help="Output directory to save videos to. Overrides global option -o/--output if set.%s"
1087+
help="Output directory to save videos to. Overrides global option -o/--output.%s"
10881088
% (USER_CONFIG.get_help_string("split-video", "output", show_default=False)),
10891089
)
10901090
@click.option(
@@ -1259,7 +1259,7 @@ def split_video_command(
12591259
"-o",
12601260
metavar="DIR",
12611261
type=click.Path(exists=False, dir_okay=True, writable=True, resolve_path=False),
1262-
help="Output directory for images. Overrides global option -o/--output if set.%s"
1262+
help="Output directory for images. Overrides global option -o/--output.%s"
12631263
% (USER_CONFIG.get_help_string("save-images", "output", show_default=False)),
12641264
)
12651265
@click.option(
@@ -1445,30 +1445,62 @@ def save_images_command(
14451445
ctx.save_images = True
14461446

14471447

1448+
@click.command("save-qp", cls=_Command)
1449+
@click.option(
1450+
"--filename",
1451+
"-f",
1452+
metavar="NAME",
1453+
default=None,
1454+
type=click.STRING,
1455+
help="Filename format to use.%s" % (USER_CONFIG.get_help_string("save-qp", "filename")),
1456+
)
1457+
@click.option(
1458+
"--output",
1459+
"-o",
1460+
metavar="DIR",
1461+
type=click.Path(exists=False, dir_okay=True, writable=True, resolve_path=False),
1462+
help="Output directory to save QP file to. Overrides global option -o/--output.%s"
1463+
% (USER_CONFIG.get_help_string("save-qp", "output", show_default=False)),
1464+
)
1465+
@click.pass_context
1466+
def save_qp_command(
1467+
ctx: click.Context,
1468+
filename: ty.Optional[ty.AnyStr],
1469+
output: ty.Optional[ty.AnyStr],
1470+
):
1471+
ctx = ctx.obj
1472+
assert isinstance(ctx, CliContext)
1473+
1474+
save_qp_args = {
1475+
"filename_format": ctx.config.get_value("save-qp", "filename", filename),
1476+
"output_dir": ctx.config.get_value("save-qp", "output", output),
1477+
}
1478+
ctx.add_command(cli_commands.save_qp, save_qp_args)
1479+
1480+
14481481
# ----------------------------------------------------------------------
1449-
# Commands Omitted From Help List
1482+
# CLI Sub-Command Registration
14501483
# ----------------------------------------------------------------------
14511484

1452-
# Info Commands
1485+
# Informational
14531486
scenedetect.add_command(about_command)
14541487
scenedetect.add_command(help_command)
14551488
scenedetect.add_command(version_command)
14561489

1457-
# ----------------------------------------------------------------------
1458-
# Commands Added To Help List
1459-
# ----------------------------------------------------------------------
1460-
1461-
# Input / Output
1462-
scenedetect.add_command(export_html_command)
1463-
scenedetect.add_command(list_scenes_command)
1490+
# Input
14641491
scenedetect.add_command(load_scenes_command)
1465-
scenedetect.add_command(save_images_command)
1466-
scenedetect.add_command(split_video_command)
14671492
scenedetect.add_command(time_command)
14681493

1469-
# Detection Algorithms
1494+
# Detectors
14701495
scenedetect.add_command(detect_adaptive_command)
14711496
scenedetect.add_command(detect_content_command)
14721497
scenedetect.add_command(detect_hash_command)
14731498
scenedetect.add_command(detect_hist_command)
14741499
scenedetect.add_command(detect_threshold_command)
1500+
1501+
# Output
1502+
scenedetect.add_command(export_html_command)
1503+
scenedetect.add_command(save_qp_command)
1504+
scenedetect.add_command(list_scenes_command)
1505+
scenedetect.add_command(save_images_command)
1506+
scenedetect.add_command(split_video_command)

scenedetect/_cli/commands.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,31 @@ def export_html(
6464
)
6565

6666

67+
def save_qp(
68+
context: CliContext,
69+
scenes: SceneList,
70+
cuts: CutList,
71+
output_dir: str,
72+
filename_format: str,
73+
):
74+
"""Handler for the `save-qp` command."""
75+
del scenes # We only use cuts for this handler.
76+
77+
qp_path = get_and_create_path(
78+
Template(filename_format).safe_substitute(VIDEO_NAME=context.video_stream.name),
79+
output_dir,
80+
)
81+
with open(qp_path, "wt") as qp_file:
82+
# TODO(#388): Instead of setting start time, should we always start at 0 and shift each
83+
# cut by the amount that was seeked?
84+
first_frame = 0 if context.start_time is None else context.start_time.frame_num
85+
# Place an initial I frame at the first frame.
86+
qp_file.write(f"{first_frame} I -1\n")
87+
# Place another I frame at each detected cut.
88+
qp_file.writelines(f"{cut.frame_num} I -1\n" for cut in cuts)
89+
logger.info(f"QP file written to: {qp_path}")
90+
91+
6792
def list_scenes(
6893
context: CliContext,
6994
scenes: SceneList,

scenedetect/_cli/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,10 @@ def format(self, timecode: FrameTimecode) -> str:
301301
"image-width": 0,
302302
"no-images": False,
303303
},
304+
"save-qp": {
305+
"filename": "$VIDEO_NAME.qp",
306+
"output": None,
307+
},
304308
"list-scenes": {
305309
"cut-format": "timecode",
306310
"display-cuts": True,

tests/test_cli.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,20 @@ def test_cli_export_html(tmp_path: Path):
430430
# TODO: Check for existence of HTML & image files.
431431

432432

433+
def test_cli_save_qp(tmp_path: Path):
434+
"""Test `save-qp` command."""
435+
base_command = "-i {VIDEO} time {TIME} {DETECTOR} {COMMAND}"
436+
assert invoke_scenedetect(base_command, COMMAND="save-qp", output_dir=tmp_path) == 0
437+
assert (
438+
invoke_scenedetect(
439+
base_command, COMMAND="save-qp --filename custom.txt", output_dir=tmp_path
440+
)
441+
== 0
442+
)
443+
assert os.path.exists(tmp_path.joinpath(f"{DEFAULT_VIDEO_NAME}.qp"))
444+
assert os.path.exists(tmp_path.joinpath("custom.txt"))
445+
446+
433447
@pytest.mark.parametrize("backend_type", ALL_BACKENDS)
434448
def test_cli_backend(backend_type: str):
435449
"""Test setting the `-b`/`--backend` argument."""

0 commit comments

Comments
 (0)