summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xar/.local/bin/ccv251
1 files changed, 251 insertions, 0 deletions
diff --git a/ar/.local/bin/ccv b/ar/.local/bin/ccv
new file mode 100755
index 0000000..bc42fc8
--- /dev/null
+++ b/ar/.local/bin/ccv
@@ -0,0 +1,251 @@
+#!/bin/sh
+set -eu
+
+prog="${0##*/}"
+
+usage() {
+ cat <<EOF
+${prog} - cut and concat video files with ffmpeg
+
+Usage:
+ ${prog} [-j | concat] [-o <output>] <file>
+ ${prog} -c | cut [-o <output>] <file> <start> [<end|duration>]
+ ${prog} -h | --help
+
+Concat (default mode):
+ Combines all files matching '<base>*.<ext>' into '<base>_combine.<ext>'.
+ Examples:
+ ${prog} clip_cut.mp4
+ ${prog} -j clip_cut.mp4 -o joined.mp4
+
+Cut:
+ <start> uses HH:MM:SS (e.g. 00:01:30).
+ Third argument is auto-detected:
+ contains ':' -> end position (ffmpeg -to)
+ pure number -> duration in seconds (ffmpeg -t)
+ omitted -> cut to end of video
+ Default output: '<base>_cut.<ext>', auto-incremented if it exists.
+ Examples:
+ ${prog} -c movie.mp4 00:12:30 # 12:30 -> end
+ ${prog} -c movie.mp4 00:12:30 00:15:00 # 12:30 -> 15:00
+ ${prog} -c movie.mp4 00:12:30 90 -o trim.mp4
+
+Output (-o):
+ May appear before or after the file argument.
+ If <output> has no extension, the input's extension is appended.
+ If the resolved output exists, you'll be asked whether to overwrite;
+ declining (default) auto-increments to '<name>_NN.<ext>'. Non-interactive
+ runs always auto-increment.
+EOF
+}
+
+die() {
+ printf '%s: %s\n' "$prog" "$1" >&2
+ exit 1
+}
+
+require_ffmpeg() {
+ command -v ffmpeg >/dev/null 2>&1 || die "ffmpeg not found in PATH"
+}
+
+# Globals set by resolve_output (avoids subshell variable loss):
+# RESOLVED_OUTPUT - final output path
+# FF_OVERWRITE - '-y' if user chose overwrite, '' otherwise
+RESOLVED_OUTPUT=""
+FF_OVERWRITE=""
+
+increment_name() {
+ _base="${1%.*}"
+ _ext="${1##*.}"
+ _n=1
+ while [ -e "${_base}_$(printf '%02d' "$_n").${_ext}" ]; do
+ _n=$((_n + 1))
+ done
+ printf '%s_%02d.%s' "$_base" "$_n" "$_ext"
+}
+
+# resolve_output <user-output> <default-extension>
+# Sets globals RESOLVED_OUTPUT and FF_OVERWRITE. Must run in current shell
+# (NOT inside $(...)) to propagate FF_OVERWRITE to the caller.
+resolve_output() {
+ _out="$1"
+ _default_ext="$2"
+ FF_OVERWRITE=""
+
+ [ -n "$_out" ] || die "-o requires a non-empty argument"
+ case "$_out" in
+ */) die "output cannot be a directory: $_out" ;;
+ *.*) ;;
+ *) _out="${_out}.${_default_ext}" ;;
+ esac
+
+ if [ -e "$_out" ]; then
+ if [ -t 0 ] && [ -t 2 ]; then
+ printf '%s: %s exists. Overwrite? [y/N] ' "$prog" "$_out" >&2
+ _ans=""
+ read _ans || _ans=""
+ case "$_ans" in
+ [yY]|[yY][eE][sS]) FF_OVERWRITE="-y" ;;
+ *) _out=$(increment_name "$_out") ;;
+ esac
+ else
+ _out=$(increment_name "$_out")
+ fi
+ fi
+ RESOLVED_OUTPUT="$_out"
+}
+
+cmd_concat() {
+ require_ffmpeg
+ input_file="$1"
+ user_output="$2"
+
+ pattern="${input_file%.*}"
+ extension="${input_file##*.}"
+ [ "$pattern" != "$input_file" ] || die "input file must have an extension: $input_file"
+
+ set +e
+ set -- "${pattern}"*."${extension}"
+ set -e
+ # Unmatched glob remains literal in POSIX sh; check via -e on the first entry.
+ if [ ! -e "$1" ]; then
+ die "no files match '${pattern}*.${extension}'"
+ fi
+ if [ $# -eq 1 ]; then
+ printf '%s: warning: only one file matches (%s); nothing to concat\n' \
+ "$prog" "$1" >&2
+ exit 1
+ fi
+
+ if [ -n "$user_output" ]; then
+ resolve_output "$user_output" "$extension"
+ output_file="$RESOLVED_OUTPUT"
+ else
+ output_file="${pattern}_combine.${extension}"
+ FF_OVERWRITE=""
+ fi
+
+ file_list=$(mktemp)
+ trap 'rm -f "$file_list"' EXIT INT HUP TERM
+
+ for video do
+ full_path=$(realpath "$video")
+ # concat demuxer: escape single quotes by closing/reopening.
+ escaped=$(printf '%s' "$full_path" | sed "s/'/'\\\\''/g")
+ printf "file '%s'\n" "$escaped" >>"$file_list"
+ done
+
+ count=$#
+ set -- ffmpeg -hide_banner
+ [ -n "$FF_OVERWRITE" ] && set -- "$@" "$FF_OVERWRITE"
+ set -- "$@" -f concat -safe 0 -i "$file_list" -c copy "$output_file"
+ "$@"
+
+ printf 'Combined %d files into %s\n' "$count" "$output_file"
+}
+
+cmd_cut() {
+ require_ffmpeg
+ file="$1"
+ start="$2"
+ end_or_dur="$3"
+ user_output="$4"
+
+ [ -f "$file" ] || die "file not found: $file"
+
+ base="${file%.*}"
+ ext="${file##*.}"
+ [ "$base" != "$file" ] || die "input file must have an extension: $file"
+
+ if [ -n "$user_output" ]; then
+ resolve_output "$user_output" "$ext"
+ out="$RESOLVED_OUTPUT"
+ else
+ FF_OVERWRITE=""
+ if [ -f "${base}_cut.${ext}" ]; then
+ n=1
+ while [ -f "${base}_cut_$(printf '%02d' "$n").${ext}" ]; do
+ n=$((n + 1))
+ done
+ out="${base}_cut_$(printf '%02d' "$n").${ext}"
+ else
+ out="${base}_cut.${ext}"
+ fi
+ fi
+
+ set -- ffmpeg -hide_banner
+ [ -n "$FF_OVERWRITE" ] && set -- "$@" "$FF_OVERWRITE"
+ set -- "$@" -ss "$start" -i "$file" -c copy
+ if [ -n "$end_or_dur" ]; then
+ case "$end_or_dur" in
+ *:*) set -- "$@" -to "$end_or_dur" ;;
+ *[!0-9.]*) die "invalid end/duration: $end_or_dur" ;;
+ *) set -- "$@" -t "$end_or_dur" ;;
+ esac
+ fi
+ set -- "$@" "$out"
+ "$@"
+
+ printf 'Created %s\n' "$out"
+}
+
+# ---- Argument parsing ----
+
+[ $# -eq 0 ] && { usage; exit 0; }
+
+mode_explicit=""
+out_override=""
+
+# Rotate args: separate flags from positionals while preserving original order
+# of positionals. After the loop, $@ holds positional args only.
+i=$#
+while [ "$i" -gt 0 ]; do
+ arg="$1"
+ shift
+ i=$((i - 1))
+ case "$arg" in
+ -h|--help)
+ usage; exit 0 ;;
+ -j|concat)
+ [ -z "$mode_explicit" ] || die "mode specified twice"
+ mode_explicit="concat" ;;
+ -c|cut)
+ [ -z "$mode_explicit" ] || die "mode specified twice"
+ mode_explicit="cut" ;;
+ -o)
+ [ "$i" -gt 0 ] || die "-o requires an argument"
+ out_override="$1"
+ shift
+ i=$((i - 1))
+ [ -n "$out_override" ] || die "-o requires a non-empty argument" ;;
+ -o=*)
+ out_override="${arg#-o=}"
+ [ -n "$out_override" ] || die "-o requires a non-empty argument" ;;
+ -*)
+ die "unknown option: $arg" ;;
+ *)
+ set -- "$@" "$arg" ;;
+ esac
+done
+
+if [ -z "$mode_explicit" ]; then
+ case $# in
+ 1) mode_explicit="concat" ;;
+ 2|3) mode_explicit="cut" ;;
+ *) usage >&2; exit 1 ;;
+ esac
+fi
+
+case "$mode_explicit" in
+ concat)
+ [ $# -eq 1 ] || die "concat takes 1 file argument, got $#"
+ cmd_concat "$1" "$out_override"
+ ;;
+ cut)
+ case $# in
+ 2) cmd_cut "$1" "$2" "" "$out_override" ;;
+ 3) cmd_cut "$1" "$2" "$3" "$out_override" ;;
+ *) die "cut takes 2 or 3 positional arguments, got $#" ;;
+ esac
+ ;;
+esac