Skip to content

Commit 17bfa7d

Browse files
committed
[cli] Centralize preconditions in CliContext
1 parent d399eb1 commit 17bfa7d

File tree

6 files changed

+203
-281
lines changed

6 files changed

+203
-281
lines changed

scenedetect/_cli/__init__.py

Lines changed: 65 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
import inspect
2222
import logging
23+
import os
24+
import os.path
2325
import typing as ty
2426

2527
import click
@@ -32,10 +34,9 @@
3234
CONFIG_MAP,
3335
DEFAULT_JPG_QUALITY,
3436
DEFAULT_WEBP_QUALITY,
35-
USER_CONFIG,
3637
TimecodeFormat,
3738
)
38-
from scenedetect._cli.context import CliContext, check_split_video_requirements
39+
from scenedetect._cli.context import USER_CONFIG, CliContext, check_split_video_requirements
3940
from scenedetect.backends import AVAILABLE_BACKENDS
4041
from scenedetect.detectors import (
4142
AdaptiveDetector,
@@ -315,8 +316,10 @@ def scenedetect(
315316
316317
Global options (e.g. -i/--input, -c/--config) must be specified before any commands and their options. The order of commands is not strict, but each command must only be specified once.
317318
"""
318-
assert isinstance(ctx.obj, CliContext)
319-
ctx.obj.handle_options(
319+
ctx = ctx.obj
320+
assert isinstance(ctx, CliContext)
321+
322+
ctx.handle_options(
320323
input_path=input,
321324
output=output,
322325
framerate=framerate,
@@ -344,7 +347,6 @@ def scenedetect(
344347
@click.pass_context
345348
def help_command(ctx: click.Context, command_name: str):
346349
"""Print help for command (`help [command]`)."""
347-
assert isinstance(ctx.obj, CliContext)
348350
assert isinstance(ctx.parent.command, click.MultiCommand)
349351
parent_command = ctx.parent.command
350352
all_commands = set(parent_command.list_commands(ctx))
@@ -368,7 +370,6 @@ def help_command(ctx: click.Context, command_name: str):
368370
@click.pass_context
369371
def about_command(ctx: click.Context):
370372
"""Print license/copyright info."""
371-
assert isinstance(ctx.obj, CliContext)
372373
click.echo("")
373374
click.echo(click.style(_LINE_SEPARATOR, fg="cyan"))
374375
click.echo(click.style(" About PySceneDetect %s" % _PROGRAM_VERSION, fg="yellow"))
@@ -381,7 +382,6 @@ def about_command(ctx: click.Context):
381382
@click.pass_context
382383
def version_command(ctx: click.Context):
383384
"""Print PySceneDetect version."""
384-
assert isinstance(ctx.obj, CliContext)
385385
click.echo("")
386386
click.echo(get_system_version_info())
387387
ctx.exit()
@@ -431,12 +431,23 @@ def time_command(
431431
432432
{scenedetect_with_video} time --start 0 --end 1000
433433
"""
434-
assert isinstance(ctx.obj, CliContext)
435-
ctx.obj.handle_time(
436-
start=start,
437-
duration=duration,
438-
end=end,
439-
)
434+
ctx = ctx.obj
435+
assert isinstance(ctx, CliContext)
436+
437+
if duration is not None and end is not None:
438+
raise click.BadParameter(
439+
"Only one of --duration/-d or --end/-e can be specified, not both.",
440+
param_hint="time",
441+
)
442+
logger.debug("Setting video time:\n start: %s, duration: %s, end: %s", start, duration, end)
443+
# *NOTE*: The Python API uses 0-based frame indices, but the CLI uses 1-based indices to
444+
# match the default start number used by `ffmpeg` when saving frames as images. As such,
445+
# we must correct start time if set as frames. See the test_cli_time* tests for for details.
446+
ctx.start_time = ctx.parse_timecode(start, correct_pts=True)
447+
ctx.end_time = ctx.parse_timecode(end)
448+
ctx.duration = ctx.parse_timecode(duration)
449+
if ctx.start_time and ctx.end_time and (ctx.start_time + 1) > ctx.end_time:
450+
raise click.BadParameter("-e/--end time must be greater than -s/--start")
440451

441452

442453
@click.command("detect-content", cls=_Command)
@@ -535,17 +546,17 @@ def detect_content_command(
535546
536547
{scenedetect_with_video} detect-content --threshold 27.5
537548
"""
538-
assert isinstance(ctx.obj, CliContext)
539-
detector_args = ctx.obj.get_detect_content_params(
549+
ctx = ctx.obj
550+
assert isinstance(ctx, CliContext)
551+
detector_args = ctx.get_detect_content_params(
540552
threshold=threshold,
541553
luma_only=luma_only,
542554
min_scene_len=min_scene_len,
543555
weights=weights,
544556
kernel_size=kernel_size,
545557
filter_mode=filter_mode,
546558
)
547-
logger.debug("Adding detector: ContentDetector(%s)", detector_args)
548-
ctx.obj.add_detector(ContentDetector(**detector_args))
559+
ctx.add_detector(ContentDetector, detector_args)
549560

550561

551562
@click.command("detect-adaptive", cls=_Command)
@@ -646,8 +657,9 @@ def detect_adaptive_command(
646657
647658
{scenedetect_with_video} detect-adaptive --threshold 3.2
648659
"""
649-
assert isinstance(ctx.obj, CliContext)
650-
detector_args = ctx.obj.get_detect_adaptive_params(
660+
ctx = ctx.obj
661+
assert isinstance(ctx, CliContext)
662+
detector_args = ctx.get_detect_adaptive_params(
651663
threshold=threshold,
652664
min_content_val=min_content_val,
653665
min_delta_hsv=min_delta_hsv,
@@ -657,8 +669,7 @@ def detect_adaptive_command(
657669
weights=weights,
658670
kernel_size=kernel_size,
659671
)
660-
logger.debug("Adding detector: AdaptiveDetector(%s)", detector_args)
661-
ctx.obj.add_detector(AdaptiveDetector(**detector_args))
672+
ctx.add_detector(AdaptiveDetector, detector_args)
662673

663674

664675
@click.command("detect-threshold", cls=_Command)
@@ -725,15 +736,15 @@ def detect_threshold_command(
725736
726737
{scenedetect_with_video} detect-threshold --threshold 15
727738
"""
728-
assert isinstance(ctx.obj, CliContext)
729-
detector_args = ctx.obj.get_detect_threshold_params(
739+
ctx = ctx.obj
740+
assert isinstance(ctx, CliContext)
741+
detector_args = ctx.get_detect_threshold_params(
730742
threshold=threshold,
731743
fade_bias=fade_bias,
732744
add_last_scene=add_last_scene,
733745
min_scene_len=min_scene_len,
734746
)
735-
logger.debug("Adding detector: ThresholdDetector(%s)", detector_args)
736-
ctx.obj.add_detector(ThresholdDetector(**detector_args))
747+
ctx.add_detector(ThresholdDetector, detector_args)
737748

738749

739750
@click.command("detect-hist", cls=_Command)
@@ -795,14 +806,12 @@ def detect_hist_command(
795806
796807
{scenedetect_with_video} detect-hist --threshold 0.1 --bins 240
797808
"""
798-
assert isinstance(ctx.obj, CliContext)
799-
800-
assert isinstance(ctx.obj, CliContext)
801-
detector_args = ctx.obj.get_detect_hist_params(
809+
ctx = ctx.obj
810+
assert isinstance(ctx, CliContext)
811+
detector_args = ctx.get_detect_hist_params(
802812
threshold=threshold, bins=bins, min_scene_len=min_scene_len
803813
)
804-
logger.debug("Adding detector: HistogramDetector(%s)", detector_args)
805-
ctx.obj.add_detector(HistogramDetector(**detector_args))
814+
ctx.add_detector(HistogramDetector, detector_args)
806815

807816

808817
@click.command("detect-hash", cls=_Command)
@@ -880,14 +889,12 @@ def detect_hash_command(
880889
881890
{scenedetect_with_video} detect-hash --size 32 --lowpass 3
882891
"""
883-
assert isinstance(ctx.obj, CliContext)
884-
885-
assert isinstance(ctx.obj, CliContext)
886-
detector_args = ctx.obj.get_detect_hash_params(
892+
ctx = ctx.obj
893+
assert isinstance(ctx, CliContext)
894+
detector_args = ctx.get_detect_hash_params(
887895
threshold=threshold, size=size, lowpass=lowpass, min_scene_len=min_scene_len
888896
)
889-
logger.debug("Adding detector: HashDetector(%s)", detector_args)
890-
ctx.obj.add_detector(HashDetector(**detector_args))
897+
ctx.add_detector(HashDetector, detector_args)
891898

892899

893900
@click.command("load-scenes", cls=_Command)
@@ -921,9 +928,23 @@ def load_scenes_command(
921928
922929
{scenedetect_with_video} load-scenes -i scenes.csv --start-col-name "Start Timecode"
923930
"""
924-
assert isinstance(ctx.obj, CliContext)
925-
logger.debug("Loading scenes from %s (start_col_name = %s)", input, start_col_name)
926-
ctx.obj.handle_load_scenes(input=input, start_col_name=start_col_name)
931+
ctx = ctx.obj
932+
assert isinstance(ctx, CliContext)
933+
934+
logger.debug("Will load scenes from %s (start_col_name = %s)", input, start_col_name)
935+
if ctx.scene_manager.get_num_detectors() > 0:
936+
raise click.ClickException("The load-scenes command cannot be used with detectors.")
937+
if ctx.load_scenes_input:
938+
raise click.ClickException("The load-scenes command must only be specified once.")
939+
input = os.path.abspath(input)
940+
if not os.path.exists(input):
941+
raise click.BadParameter(
942+
f"Could not load scenes, file does not exist: {input}", param_hint="-i/--input"
943+
)
944+
ctx.load_scenes_input = input
945+
ctx.load_scenes_column_name = ctx.config.get_value(
946+
"load-scenes", "start-col-name", start_col_name
947+
)
927948

928949

929950
@click.command("export-html", cls=_Command)
@@ -970,7 +991,7 @@ def export_html_command(
970991
"""Export scene list to HTML file. Requires save-images unless --no-images is specified."""
971992
ctx = ctx.obj
972993
assert isinstance(ctx, CliContext)
973-
ctx.ensure_input_open()
994+
974995
no_images = no_images or ctx.config.get_value("export-html", "no-images")
975996
if not ctx.save_images and not no_images:
976997
raise click.BadArgumentUsage(
@@ -1037,7 +1058,7 @@ def list_scenes_command(
10371058
"""Create scene list CSV file (will be named $VIDEO_NAME-Scenes.csv by default)."""
10381059
ctx = ctx.obj
10391060
assert isinstance(ctx, CliContext)
1040-
ctx.ensure_input_open()
1061+
10411062
no_output_file = no_output_file or ctx.config.get_value("list-scenes", "no-output-file")
10421063
scene_list_dir = ctx.config.get_value("list-scenes", "output", output, ignore_default=True)
10431064
scene_list_name_format = ctx.config.get_value("list-scenes", "filename", filename)
@@ -1162,7 +1183,7 @@ def split_video_command(
11621183
"""
11631184
ctx = ctx.obj
11641185
assert isinstance(ctx, CliContext)
1165-
ctx.ensure_input_open()
1186+
11661187
check_split_video_requirements(use_mkvmerge=mkvmerge)
11671188
if "%" in ctx.video_stream.path or "://" in ctx.video_stream.path:
11681189
error = "The split-video command is incompatible with image sequences/URLs."
@@ -1362,7 +1383,7 @@ def save_images_command(
13621383
"""
13631384
ctx = ctx.obj
13641385
assert isinstance(ctx, CliContext)
1365-
ctx.ensure_input_open()
1386+
13661387
if "://" in ctx.video_stream.path:
13671388
error_str = "\nThe save-images command is incompatible with URLs."
13681389
logger.error(error_str)

0 commit comments

Comments
 (0)