Compare commits

...

2 commits

Author SHA1 Message Date
dabruh
b1b8ce749b refactor: standardize indentation in videncode script 2025-11-17 20:06:18 +01:00
dabruh
530c9307d4 feat: add audio mode option to videncode script 2025-11-17 20:04:14 +01:00

View file

@ -18,7 +18,7 @@ log_error() { echo -e "${RED}ERROR:${NC} $1" >&2; }
# Gets the duration of a video file in seconds using ffprobe. # Gets the duration of a video file in seconds using ffprobe.
get_duration() { get_duration() {
ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$1" 2>/dev/null || echo "0" ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$1" 2>/dev/null || echo "0"
} }
# Default encoding parameters tuned for quality and compatibility. # Default encoding parameters tuned for quality and compatibility.
@ -44,10 +44,11 @@ OVERWRITE_DIFFERENT=false
IS_BATCH=false IS_BATCH=false
INPUT_DIR="" INPUT_DIR=""
INPUT_FILES=() INPUT_FILES=()
AUDIO_MODE="transcode"
# Prints usage information to stdout. # Prints usage information to stdout.
usage() { usage() {
cat <<EOF cat <<EOF
Usage: $(basename "$0") -i input.mp4 [options] Usage: $(basename "$0") -i input.mp4 [options]
Options: Options:
@ -62,173 +63,226 @@ Usage: $(basename "$0") -i input.mp4 [options]
--max-h Maximum height in pixels (default: ${DEFAULT_MAX_H}) --max-h Maximum height in pixels (default: ${DEFAULT_MAX_H})
--overwrite-same Overwrite output even if duration matches input --overwrite-same Overwrite output even if duration matches input
--overwrite-different Overwrite output even if duration differs from input --overwrite-different Overwrite output even if duration differs from input
--audio-mode Audio mode: transcode (default) or copy
-h, --help Show this help -h, --help Show this help
EOF EOF
} }
# Parses command-line arguments, supporting both short and long options. # Parses command-line arguments, supporting both short and long options.
parse_args() { parse_args() {
if [[ $# -eq 0 ]]; then if [[ $# -eq 0 ]]; then
usage usage
exit 1 exit 1
fi fi
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
-i|--input) INFILE="$2"; shift 2 ;; -i | --input)
-o|--output) OUTFILE="$2"; shift 2 ;; INFILE="$2"
-c|--crf) CRF="$2"; shift 2 ;; shift 2
-p|--preset) PRESET="$2"; shift 2 ;; ;;
-m|--maxrate) MAXRATE="$2"; shift 2 ;; -o | --output)
-b|--bufsize) BUFSIZE="$2"; shift 2 ;; OUTFILE="$2"
--extra) EXTRA_ARGS+=("$2"); shift 2 ;; shift 2
--max-w) MAX_W="$2"; shift 2 ;; ;;
--max-h) MAX_H="$2"; shift 2 ;; -c | --crf)
--overwrite-same) OVERWRITE_SAME=true; shift ;; CRF="$2"
--overwrite-different) OVERWRITE_DIFFERENT=true; shift ;; shift 2
-h|--help) usage; exit 0 ;; ;;
--) shift; break ;; -p | --preset)
*) echo "Unknown argument: $1" >&2; usage; exit 1 ;; PRESET="$2"
esac shift 2
done ;;
-m | --maxrate)
MAXRATE="$2"
shift 2
;;
-b | --bufsize)
BUFSIZE="$2"
shift 2
;;
--extra)
EXTRA_ARGS+=("$2")
shift 2
;;
--max-w)
MAX_W="$2"
shift 2
;;
--max-h)
MAX_H="$2"
shift 2
;;
--overwrite-same)
OVERWRITE_SAME=true
shift
;;
--overwrite-different)
OVERWRITE_DIFFERENT=true
shift
;;
--audio-mode)
AUDIO_MODE="$2"
shift 2
;;
-h | --help)
usage
exit 0
;;
--)
shift
break
;;
*)
echo "Unknown argument: $1" >&2
usage
exit 1
;;
esac
done
} }
# Validates input and determines output path based on rules to avoid overwriting. # Validates input and determines output path based on rules to avoid overwriting.
validate_input() { validate_input() {
if [[ -z "${INFILE:-}" ]]; then if [[ -z "${INFILE:-}" ]]; then
log_error "input (-i/--input) is required." log_error "input (-i/--input) is required."
usage usage
exit 1 exit 1
fi fi
if [[ -d "$INFILE" ]]; then if [[ -d "$INFILE" ]]; then
IS_BATCH=true IS_BATCH=true
INPUT_DIR="$INFILE" INPUT_DIR="$INFILE"
log_info "Processing directory: $INFILE" log_info "Processing directory: $INFILE"
# Find video files in the directory # Find video files in the directory
while IFS= read -r -d '' file; do while IFS= read -r -d '' file; do
INPUT_FILES+=("$file") INPUT_FILES+=("$file")
done < <(find "$INPUT_DIR" -type f \( -iname "*.mp4" -o -iname "*.avi" -o -iname "*.mkv" -o -iname "*.mov" -o -iname "*.flv" -o -iname "*.wmv" -o -iname "*.webm" -o -iname "*.m4v" \) -print0) done < <(find "$INPUT_DIR" -type f \( -iname "*.mp4" -o -iname "*.avi" -o -iname "*.mkv" -o -iname "*.mov" -o -iname "*.flv" -o -iname "*.wmv" -o -iname "*.webm" -o -iname "*.m4v" \) -print0)
if [[ ${#INPUT_FILES[@]} -eq 0 ]]; then if [[ ${#INPUT_FILES[@]} -eq 0 ]]; then
log_error "no video files found in directory: $INFILE" log_error "no video files found in directory: $INFILE"
exit 1 exit 1
fi fi
log_info "Found ${#INPUT_FILES[@]} video file(s) to process" log_info "Found ${#INPUT_FILES[@]} video file(s) to process"
elif [[ -f "$INFILE" ]]; then elif [[ -f "$INFILE" ]]; then
IS_BATCH=false IS_BATCH=false
INPUT_FILES=("$INFILE") INPUT_FILES=("$INFILE")
log_info "Processing single file: $INFILE" log_info "Processing single file: $INFILE"
else else
log_error "input not found or not a file/directory: $INFILE" log_error "input not found or not a file/directory: $INFILE"
exit 1 exit 1
fi fi
} }
# Validates single input file and determines output path. # Validates single input file and determines output path.
validate_input_single() { validate_input_single() {
# Determine output path: use _videncode.mp4 suffix in same dir, original name in different dir. # Determine output path: use _videncode.mp4 suffix in same dir, original name in different dir.
if [[ -n "$OUTFILE" && -d "$OUTFILE" ]]; then if [[ -n "$OUTFILE" && -d "$OUTFILE" ]]; then
if [[ "$OUTFILE" == "$INPUT_DIR" ]]; then if [[ "$OUTFILE" == "$INPUT_DIR" ]]; then
OUTFILE="${OUTFILE%/}/$(basename "${INFILE%.*}")_videncode.mp4" OUTFILE="${OUTFILE%/}/$(basename "${INFILE%.*}")_videncode.mp4"
else else
OUTFILE="${OUTFILE%/}/$(basename "$INFILE")" OUTFILE="${OUTFILE%/}/$(basename "$INFILE")"
fi fi
elif [[ -z "$OUTFILE" ]]; then elif [[ -z "$OUTFILE" ]]; then
OUTFILE="${INFILE%.*}_videncode.mp4" OUTFILE="${INFILE%.*}_videncode.mp4"
fi fi
log_info "Output will be: $OUTFILE" log_info "Output will be: $OUTFILE"
# Prevent accidental overwriting of the input file. # Prevent accidental overwriting of the input file.
if [[ "$OUTFILE" == "$INFILE" ]]; then if [[ "$OUTFILE" == "$INFILE" ]]; then
log_error "output file would overwrite input file." log_error "output file would overwrite input file."
exit 1 exit 1
fi fi
} }
# Assembles the full ffmpeg command array with all configured options. # Assembles the full ffmpeg command array with all configured options.
build_ffmpeg_cmd() { build_ffmpeg_cmd() {
# Sets audio flags to copy streams unchanged for fidelity. # Sets audio flags to transcode to Opus or copy streams unchanged.
local audio_flags=("-c:a" "copy") local audio_flags
if [[ "$AUDIO_MODE" == "transcode" ]]; then
audio_flags=("-c:a" "libopus")
else
audio_flags=("-c:a" "copy")
fi
# Constructs the ffmpeg video scaling filter to fit within max dimensions while preserving aspect ratio. # Constructs the ffmpeg video scaling filter to fit within max dimensions while preserving aspect ratio.
# Scale logic: for landscape (w>h), constrain width to MAX_W and height to MAX_H; # Scale logic: for landscape (w>h), constrain width to MAX_W and height to MAX_H;
# for portrait (h>w), constrain width to MAX_H and height to MAX_W. # for portrait (h>w), constrain width to MAX_H and height to MAX_W.
# Commas are escaped for safe shell passing; force_original_aspect_ratio=decrease avoids upscaling. # Commas are escaped for safe shell passing; force_original_aspect_ratio=decrease avoids upscaling.
local vf="scale='if(gt(iw\\,ih)\\,min(iw\\,${MAX_W})\\,min(iw\\,${MAX_H}))':'if(gt(iw\\,ih)\\,min(ih\\,${MAX_H})\\,min(ih\\,${MAX_W}))':force_original_aspect_ratio=decrease" local vf="scale='if(gt(iw\\,ih)\\,min(iw\\,${MAX_W})\\,min(iw\\,${MAX_H}))':'if(gt(iw\\,ih)\\,min(ih\\,${MAX_H})\\,min(ih\\,${MAX_W}))':force_original_aspect_ratio=decrease"
CMD=(ffmpeg -y -i "$INFILE" -map_metadata 0 -c:v libx264 -preset "$PRESET" -crf "$CRF" -maxrate "$MAXRATE" -bufsize "$BUFSIZE" -vf "$vf" "${audio_flags[@]}" -movflags +faststart) CMD=(ffmpeg -y -i "$INFILE" -map_metadata 0 -c:v libx264 -preset "$PRESET" -crf "$CRF" -maxrate "$MAXRATE" -bufsize "$BUFSIZE" -vf "$vf" "${audio_flags[@]}" -movflags +faststart)
# Safely append user-provided extra arguments by splitting on whitespace. # Safely append user-provided extra arguments by splitting on whitespace.
if [[ ${#EXTRA_ARGS[@]} -gt 0 ]]; then if [[ ${#EXTRA_ARGS[@]} -gt 0 ]]; then
for ea in "${EXTRA_ARGS[@]}"; do for ea in "${EXTRA_ARGS[@]}"; do
read -r -a split <<< "$ea" read -r -a split <<<"$ea"
CMD+=("${split[@]}") CMD+=("${split[@]}")
done done
fi fi
CMD+=("$OUTFILE") CMD+=("$OUTFILE")
} }
# Executes the constructed ffmpeg command after displaying it for transparency. # Executes the constructed ffmpeg command after displaying it for transparency.
run_ffmpeg() { run_ffmpeg() {
log_info "Processing: $INFILE -> $OUTFILE" log_info "Processing: $INFILE -> $OUTFILE"
if [[ -f "$OUTFILE" ]]; then if [[ -f "$OUTFILE" ]]; then
input_duration=$(get_duration "$INFILE") input_duration=$(get_duration "$INFILE")
output_duration=$(get_duration "$OUTFILE") output_duration=$(get_duration "$OUTFILE")
if [[ "$input_duration" == "$output_duration" && "$output_duration" != "0" ]]; then if [[ "$input_duration" == "$output_duration" && "$output_duration" != "0" ]]; then
if [[ "$OVERWRITE_SAME" == false ]]; then if [[ "$OVERWRITE_SAME" == false ]]; then
log_warn "Skipping existing file with matching duration: $OUTFILE" log_warn "Skipping existing file with matching duration: $OUTFILE"
return return
fi fi
elif [[ "$IS_BATCH" == false ]]; then elif [[ "$IS_BATCH" == false ]]; then
if [[ "$OVERWRITE_DIFFERENT" == false ]]; then if [[ "$OVERWRITE_DIFFERENT" == false ]]; then
read -p "Output file exists with different duration, overwrite? (y/N) " -n 1 -r read -p "Output file exists with different duration, overwrite? (y/N) " -n 1 -r
echo echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then if [[ ! $REPLY =~ ^[Yy]$ ]]; then
log_warn "User chose not to overwrite: $OUTFILE" log_warn "User chose not to overwrite: $OUTFILE"
return return
fi fi
fi fi
else else
if [[ "$OVERWRITE_DIFFERENT" == false ]]; then if [[ "$OVERWRITE_DIFFERENT" == false ]]; then
log_warn "Skipping existing file with different duration: $OUTFILE" log_warn "Skipping existing file with different duration: $OUTFILE"
return return
fi fi
fi fi
fi fi
log_info "Starting encoding..." log_info "Starting encoding..."
log_info "Command:" log_info "Command:"
printf ' %q' "${CMD[@]}" printf ' %q' "${CMD[@]}"
echo echo
"${CMD[@]}" "${CMD[@]}"
log_success "Encoding completed: $OUTFILE" log_success "Encoding completed: $OUTFILE"
} }
# Main entry point: orchestrates parsing, validation, command building, and execution. # Main entry point: orchestrates parsing, validation, command building, and execution.
main() { main() {
parse_args "$@" parse_args "$@"
validate_input validate_input
if [[ "$IS_BATCH" == true ]]; then if [[ "$IS_BATCH" == true ]]; then
# Save the output directory for batch processing # Save the output directory for batch processing
local OUTPUT_DIR="$OUTFILE" local OUTPUT_DIR="$OUTFILE"
local total=${#INPUT_FILES[@]} local total=${#INPUT_FILES[@]}
local count=1 local count=1
for file in "${INPUT_FILES[@]}"; do for file in "${INPUT_FILES[@]}"; do
log_info "Processing file $count of $total: $(basename "$file")" log_info "Processing file $count of $total: $(basename "$file")"
INFILE="$file" INFILE="$file"
OUTFILE="$OUTPUT_DIR" # Reset OUTFILE for each iteration OUTFILE="$OUTPUT_DIR" # Reset OUTFILE for each iteration
validate_input_single validate_input_single
build_ffmpeg_cmd build_ffmpeg_cmd
run_ffmpeg run_ffmpeg
((count++)) ((count++))
done done
log_success "Batch processing completed: $total file(s) processed" log_success "Batch processing completed: $total file(s) processed"
else else
validate_input_single validate_input_single
build_ffmpeg_cmd build_ffmpeg_cmd
run_ffmpeg run_ffmpeg
fi fi
} }
main "$@" main "$@"