Skip to content

Commit

Permalink
[cli] Centralize preconditions in CliContext
Browse files Browse the repository at this point in the history
  • Loading branch information
Breakthrough committed Sep 28, 2024
1 parent a99aed1 commit ded3708
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 280 deletions.
109 changes: 65 additions & 44 deletions scenedetect/_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

import inspect
import logging
import os
import os.path
import typing as ty

import click
Expand All @@ -32,10 +34,9 @@
CONFIG_MAP,
DEFAULT_JPG_QUALITY,
DEFAULT_WEBP_QUALITY,
USER_CONFIG,
TimecodeFormat,
)
from scenedetect._cli.context import CliContext, check_split_video_requirements
from scenedetect._cli.context import USER_CONFIG, CliContext, check_split_video_requirements
from scenedetect.backends import AVAILABLE_BACKENDS
from scenedetect.detectors import (
AdaptiveDetector,
Expand Down Expand Up @@ -315,8 +316,10 @@ def scenedetect(
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.
"""
assert isinstance(ctx.obj, CliContext)
ctx.obj.handle_options(
ctx = ctx.obj
assert isinstance(ctx, CliContext)

ctx.handle_options(
input_path=input,
output=output,
framerate=framerate,
Expand Down Expand Up @@ -344,7 +347,6 @@ def scenedetect(
@click.pass_context
def help_command(ctx: click.Context, command_name: str):
"""Print help for command (`help [command]`)."""
assert isinstance(ctx.obj, CliContext)
assert isinstance(ctx.parent.command, click.MultiCommand)
parent_command = ctx.parent.command
all_commands = set(parent_command.list_commands(ctx))
Expand All @@ -368,7 +370,6 @@ def help_command(ctx: click.Context, command_name: str):
@click.pass_context
def about_command(ctx: click.Context):
"""Print license/copyright info."""
assert isinstance(ctx.obj, CliContext)
click.echo("")
click.echo(click.style(_LINE_SEPARATOR, fg="cyan"))
click.echo(click.style(" About PySceneDetect %s" % _PROGRAM_VERSION, fg="yellow"))
Expand All @@ -381,7 +382,6 @@ def about_command(ctx: click.Context):
@click.pass_context
def version_command(ctx: click.Context):
"""Print PySceneDetect version."""
assert isinstance(ctx.obj, CliContext)
click.echo("")
click.echo(get_system_version_info())
ctx.exit()
Expand Down Expand Up @@ -431,12 +431,23 @@ def time_command(
{scenedetect_with_video} time --start 0 --end 1000
"""
assert isinstance(ctx.obj, CliContext)
ctx.obj.handle_time(
start=start,
duration=duration,
end=end,
)
ctx = ctx.obj
assert isinstance(ctx, CliContext)

if duration is not None and end is not None:
raise click.BadParameter(
"Only one of --duration/-d or --end/-e can be specified, not both.",
param_hint="time",
)
logger.debug("Setting video time:\n start: %s, duration: %s, end: %s", start, duration, end)
# *NOTE*: The Python API uses 0-based frame indices, but the CLI uses 1-based indices to
# match the default start number used by `ffmpeg` when saving frames as images. As such,
# we must correct start time if set as frames. See the test_cli_time* tests for for details.
ctx.start_time = ctx.parse_timecode(start, correct_pts=True)
ctx.end_time = ctx.parse_timecode(end)
ctx.duration = ctx.parse_timecode(duration)
if ctx.start_time and ctx.end_time and (ctx.start_time + 1) > ctx.end_time:
raise click.BadParameter("-e/--end time must be greater than -s/--start")


@click.command("detect-content", cls=_Command)
Expand Down Expand Up @@ -535,17 +546,17 @@ def detect_content_command(
{scenedetect_with_video} detect-content --threshold 27.5
"""
assert isinstance(ctx.obj, CliContext)
detector_args = ctx.obj.get_detect_content_params(
ctx = ctx.obj
assert isinstance(ctx, CliContext)
detector_args = ctx.get_detect_content_params(
threshold=threshold,
luma_only=luma_only,
min_scene_len=min_scene_len,
weights=weights,
kernel_size=kernel_size,
filter_mode=filter_mode,
)
logger.debug("Adding detector: ContentDetector(%s)", detector_args)
ctx.obj.add_detector(ContentDetector(**detector_args))
ctx.add_detector(ContentDetector, detector_args)


@click.command("detect-adaptive", cls=_Command)
Expand Down Expand Up @@ -646,8 +657,9 @@ def detect_adaptive_command(
{scenedetect_with_video} detect-adaptive --threshold 3.2
"""
assert isinstance(ctx.obj, CliContext)
detector_args = ctx.obj.get_detect_adaptive_params(
ctx = ctx.obj
assert isinstance(ctx, CliContext)
detector_args = ctx.get_detect_adaptive_params(
threshold=threshold,
min_content_val=min_content_val,
min_delta_hsv=min_delta_hsv,
Expand All @@ -657,8 +669,7 @@ def detect_adaptive_command(
weights=weights,
kernel_size=kernel_size,
)
logger.debug("Adding detector: AdaptiveDetector(%s)", detector_args)
ctx.obj.add_detector(AdaptiveDetector(**detector_args))
ctx.add_detector(AdaptiveDetector, detector_args)


@click.command("detect-threshold", cls=_Command)
Expand Down Expand Up @@ -725,15 +736,15 @@ def detect_threshold_command(
{scenedetect_with_video} detect-threshold --threshold 15
"""
assert isinstance(ctx.obj, CliContext)
detector_args = ctx.obj.get_detect_threshold_params(
ctx = ctx.obj
assert isinstance(ctx, CliContext)
detector_args = ctx.get_detect_threshold_params(
threshold=threshold,
fade_bias=fade_bias,
add_last_scene=add_last_scene,
min_scene_len=min_scene_len,
)
logger.debug("Adding detector: ThresholdDetector(%s)", detector_args)
ctx.obj.add_detector(ThresholdDetector(**detector_args))
ctx.add_detector(ThresholdDetector, detector_args)


@click.command("detect-hist", cls=_Command)
Expand Down Expand Up @@ -795,14 +806,12 @@ def detect_hist_command(
{scenedetect_with_video} detect-hist --threshold 0.1 --bins 240
"""
assert isinstance(ctx.obj, CliContext)

assert isinstance(ctx.obj, CliContext)
detector_args = ctx.obj.get_detect_hist_params(
ctx = ctx.obj
assert isinstance(ctx, CliContext)
detector_args = ctx.get_detect_hist_params(
threshold=threshold, bins=bins, min_scene_len=min_scene_len
)
logger.debug("Adding detector: HistogramDetector(%s)", detector_args)
ctx.obj.add_detector(HistogramDetector(**detector_args))
ctx.add_detector(HistogramDetector, detector_args)


@click.command("detect-hash", cls=_Command)
Expand Down Expand Up @@ -880,14 +889,12 @@ def detect_hash_command(
{scenedetect_with_video} detect-hash --size 32 --lowpass 3
"""
assert isinstance(ctx.obj, CliContext)

assert isinstance(ctx.obj, CliContext)
detector_args = ctx.obj.get_detect_hash_params(
ctx = ctx.obj
assert isinstance(ctx, CliContext)
detector_args = ctx.get_detect_hash_params(
threshold=threshold, size=size, lowpass=lowpass, min_scene_len=min_scene_len
)
logger.debug("Adding detector: HashDetector(%s)", detector_args)
ctx.obj.add_detector(HashDetector(**detector_args))
ctx.add_detector(HashDetector, detector_args)


@click.command("load-scenes", cls=_Command)
Expand Down Expand Up @@ -921,9 +928,23 @@ def load_scenes_command(
{scenedetect_with_video} load-scenes -i scenes.csv --start-col-name "Start Timecode"
"""
assert isinstance(ctx.obj, CliContext)
logger.debug("Loading scenes from %s (start_col_name = %s)", input, start_col_name)
ctx.obj.handle_load_scenes(input=input, start_col_name=start_col_name)
ctx = ctx.obj
assert isinstance(ctx, CliContext)

logger.debug("Will load scenes from %s (start_col_name = %s)", input, start_col_name)
if ctx.scene_manager.get_num_detectors() > 0:
raise click.ClickException("The load-scenes command cannot be used with detectors.")
if ctx.load_scenes_input:
raise click.ClickException("The load-scenes command must only be specified once.")
input = os.path.abspath(input)
if not os.path.exists(input):
raise click.BadParameter(
f"Could not load scenes, file does not exist: {input}", param_hint="-i/--input"
)
ctx.load_scenes_input = input
ctx.load_scenes_column_name = ctx.config.get_value(
"load-scenes", "start-col-name", start_col_name
)


@click.command("export-html", cls=_Command)
Expand Down Expand Up @@ -970,7 +991,7 @@ def export_html_command(
"""Export scene list to HTML file. Requires save-images unless --no-images is specified."""
ctx = ctx.obj
assert isinstance(ctx, CliContext)
ctx.ensure_input_open()

no_images = no_images or ctx.config.get_value("export-html", "no-images")
if not ctx.save_images and not no_images:
raise click.BadArgumentUsage(
Expand Down Expand Up @@ -1037,7 +1058,7 @@ def list_scenes_command(
"""Create scene list CSV file (will be named $VIDEO_NAME-Scenes.csv by default)."""
ctx = ctx.obj
assert isinstance(ctx, CliContext)
ctx.ensure_input_open()

no_output_file = no_output_file or ctx.config.get_value("list-scenes", "no-output-file")
scene_list_dir = ctx.config.get_value("list-scenes", "output", output, ignore_default=True)
scene_list_name_format = ctx.config.get_value("list-scenes", "filename", filename)
Expand Down Expand Up @@ -1162,7 +1183,7 @@ def split_video_command(
"""
ctx = ctx.obj
assert isinstance(ctx, CliContext)
ctx.ensure_input_open()

check_split_video_requirements(use_mkvmerge=mkvmerge)
if "%" in ctx.video_stream.path or "://" in ctx.video_stream.path:
error = "The split-video command is incompatible with image sequences/URLs."
Expand Down Expand Up @@ -1362,7 +1383,7 @@ def save_images_command(
"""
ctx = ctx.obj
assert isinstance(ctx, CliContext)
ctx.ensure_input_open()

if "://" in ctx.video_stream.path:
error_str = "\nThe save-images command is incompatible with URLs."
logger.error(error_str)
Expand Down
Loading

0 comments on commit ded3708

Please sign in to comment.