Skip to content

Instantly share code, notes, and snippets.

@evoactivity
Created January 4, 2025 07:06
Show Gist options
  • Save evoactivity/6720aef02391b4fa761a44e7e4812585 to your computer and use it in GitHub Desktop.
Save evoactivity/6720aef02391b4fa761a44e7e4812585 to your computer and use it in GitHub Desktop.
Batch detect video orientation
#!/bin/bash
set -euo pipefail
usage() {
echo -e "This script will use ffprobe to detect the orientation of a video file and will output the findings as specified by the options.
Usage: $0 [directory] [OPTIONS]
If no directory is specified, the current directory will be used.
Options:
-f, --filter <landscape|portrait> Filter results based on video orientation
-l Filter results for landscape videos
-p Filter results for portrait videos
-r, --relative Show relative file paths instead of full paths
-c, --concurrent <number> Number of concurrent jobs to run (default: 1)
-h, --help Show this help message and exit
Short flags can be combined in any order, e.g., -lr or -rl for landscape filter with relative paths, but -l and -p cannot be used together.
Examples of usage:
Detect all videos in the current directory and show results with full paths:
$ $0
File: /Users/example/Desktop/video1.mp4
Orientation: Landscape
-------------------------
File: /Users/example/Desktop/video2.mp4
Orientation: Portrait
-------------------------
Detect videos and filter only landscape with relative paths:
$ $0 -lr
./video1.mp4
./video2.mp4
./video3.mp4
Detect videos and filter only portrait with 4 concurrent jobs:
$ $0 -p -c 4
/Users/example/videos/video6.mp4
/Users/example/videos/video9.mp4
Detect videos with full paths and limit to 2 concurrent jobs:
$ $0 ./videos --concurrent 2
File: /Users/example/videos/video6.mp4
Orientation: Landscape
-------------------------
File: /Users/example/videos/video2.mp4
Orientation: Landscape
-------------------------
"
exit 0
}
# Check for help option before processing arguments
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
usage
fi
# Default values
declare SCAN_DIR="./"
declare OUTPUT_FILTER=""
declare RELATIVE_PATHS=false
declare -i CONCURRENT_JOBS=1
# Set SCAN_DIR as a positional argument if provided
if [[ -n "${1:-}" && "${1:-}" != -* ]]; then
SCAN_DIR="$1"
shift
fi
# Initialize filter check to avoid -l and -p being used together
USED_L=false
USED_P=false
LP_ERROR_MESSAGE="Error: Cannot filter for both landscape and portrait at the same time."
# Parse options allowing combined short flags
while [[ "$#" -gt 0 ]]; do
if [[ "$1" =~ ^-[lprh]+$ && ! "$1" =~ ^-- ]]; then
if [[ "$1" == *"h"* ]]; then
usage
fi
# Prevent using both landscape and portrait filters together
if [[ "$1" == *"l"* && "$1" == *"p"* ]]; then
echo $LP_ERROR_MESSAGE
exit 1
fi
# Loop through characters in the combined short flag
for (( i=1; i<${#1}; i++ )); do
char="${1:$i:1}"
case $char in
l)
if "$USED_P" == true; then
echo $LP_ERROR_MESSAGE
exit 1
fi
OUTPUT_FILTER="landscape"
USED_L=true
;;
p)
if "$USED_L" == true; then
echo $LP_ERROR_MESSAGE
exit 1
fi
OUTPUT_FILTER="portrait"
USED_P=true
;;
r) RELATIVE_PATHS=true ;;
*)
echo "Unknown option: -$char"
exit 1
;;
esac
done
shift
elif [[ "$1" == "-c" || "$1" == "--concurrent" ]]; then
if [[ -n "$2" && "$2" =~ ^[0-9]+$ ]]; then
CONCURRENT_JOBS="$2"
shift 2
else
echo "Error: -c requires a numeric value."
exit 1
fi
elif [[ "$1" == "-f" || "$1" == "--filter" ]]; then
if [[ -n "$2" && ( "$2" != "landscape" || "$2" != "portrait" ) ]]; then
echo "Error: --filter requires 'landscape' or 'portrait' as a value."
exit 1
fi
if [[ ( "$2" == "landscape" && "$USED_P" == true ) || ( "$2" == "portrait" && "$USED_L" == true ) ]]; then
echo $LP_ERROR_MESSAGE
exit 1
fi
OUTPUT_FILTER="$2"
if [[ "$2" == "landscape" ]]; then
USED_L=true
else
USED_P=true
fi
shift 2
elif [[ "$1" == "-r" || "$1" == "--relative" ]]; then
RELATIVE_PATHS=true
shift
elif [[ "$1" == "-h" || "$1" == "--help" ]]; then
usage
else
echo "Unknown option: $1"
exit 1
fi
done
# Function to process a single file
process_file() {
local file="$1"
# Fast check for video files using `file` command
if file --mime-type "$file" | grep -q 'video/'; then
# Attempt to get dimensions and fail silently
local dimensions=$(ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 "$file" 2>/dev/null)
# If dimensions are empty, skip the file
if [[ -z "$dimensions" ]]; then
return
fi
local width=$(echo "$dimensions" | cut -d',' -f1)
local height=$(echo "$dimensions" | cut -d',' -f2)
if (( width > height )); then
local orientation="Landscape"
else
local orientation="Portrait"
fi
local orientation_lower="$(echo "$orientation" | tr '[:upper:]' '[:lower:]')"
if [[ -n "$OUTPUT_FILTER" && "$OUTPUT_FILTER" == "$orientation_lower" ]]; then
if $RELATIVE_PATHS; then
echo "${file#$SCAN_DIR/}"
else
echo "$(realpath "$file")"
fi
elif [[ -z "$OUTPUT_FILTER" ]]; then
if $RELATIVE_PATHS; then
echo "File: ${file#$SCAN_DIR/}"
else
echo "File: $(realpath "$file")"
fi
echo "Orientation: $orientation"
echo "-------------------------"
fi
else
echo "Skipping non-video file: $file" >&2
return
fi
}
# Function to find video files using a null-terminated list
find_video_files() {
find "$SCAN_DIR" -maxdepth 1 -type f \( \
-iname "*.mp4" -o \
-iname "*.mkv" -o \
-iname "*.avi" -o \
-iname "*.mov" -o \
-iname "*.flv" -o \
-iname "*.wmv" -o \
-iname "*.webm" -o \
-iname "*.mpeg" -o \
-iname "*.mpg" -o \
-iname "*.3gp" -o \
-iname "*.ogv" -o \
-iname "*.m4v" -o \
-iname "*.mts" -o \
-iname "*.ts" -o \
-iname "*.vob" -o \
-iname "*.f4v" -o \
-iname "*.rm" -o \
-iname "*.rmvb" -o \
-iname "*.divx" -o \
-iname "*.asf" -o \
-iname "*.m2ts" -o \
-iname "*.dv" -o \
-iname "*.3g2" -o \
-iname "*.mxf" -o \
-iname "*.dat" -o \
-iname "*.ivf" -o \
-iname "*.yuv" -o \
-iname "*.r3d" \
\) -print0
}
# If using concurrency use parallel
if (( CONCURRENT_JOBS > 1 )); then
# Exports to be used by parallel
export -f process_file
export OUTPUT_FILTER RELATIVE_PATHS SCAN_DIR
find_video_files | parallel --keep-order -0 -j "$CONCURRENT_JOBS" process_file
else
find_video_files | while IFS= read -r -d '' file; do
process_file "$file"
done
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment