Created
January 4, 2025 07:06
-
-
Save evoactivity/6720aef02391b4fa761a44e7e4812585 to your computer and use it in GitHub Desktop.
Batch detect video orientation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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