Adding handoff functionality for reviews
This commit is contained in:
Executable
+300
@@ -0,0 +1,300 @@
|
||||
#!/bin/zsh
|
||||
#
|
||||
# Video Transcoding Script
|
||||
# Converts video files to H.265/HEVC at 1080p for significant size reduction
|
||||
#
|
||||
# Usage: ./transcode.sh <folder> [options]
|
||||
#
|
||||
# Options:
|
||||
# --replace Delete original files after successful transcoding
|
||||
# --dry-run Show what would be transcoded without doing it
|
||||
# --crf <N> Quality level (default: 23, lower=better quality, 18-28 typical)
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
DEFAULT_CRF=23
|
||||
EXTENSIONS=("mov" "mp4" "m4v" "avi" "mkv" "mxf")
|
||||
|
||||
usage() {
|
||||
cat << EOF
|
||||
Video Transcoding Script
|
||||
|
||||
Converts video files to H.265/HEVC at 1080p for significant size reduction.
|
||||
Typically achieves 80-95% size reduction from uncompressed 4K footage.
|
||||
|
||||
Usage: $(basename "$0") <folder|file> [options]
|
||||
|
||||
Options:
|
||||
--replace Delete original files after successful transcoding
|
||||
--dry-run Show what would be transcoded without doing it
|
||||
--crf <N> Quality level (default: 23)
|
||||
Lower = better quality, larger files
|
||||
18 = visually lossless, 23 = default, 28 = smaller
|
||||
--help Show this help message
|
||||
|
||||
Output:
|
||||
Files are saved alongside originals with '_compressed.mp4' suffix.
|
||||
With --replace, originals are deleted after successful transcode.
|
||||
When processing a folder, files are sorted smallest-first.
|
||||
|
||||
Examples:
|
||||
$(basename "$0") ./video.mov # Transcode single file
|
||||
$(basename "$0") ./media/videos # Transcode folder (smallest first)
|
||||
$(basename "$0") ./media/videos --dry-run # Preview only
|
||||
$(basename "$0") ./media/videos --replace # Transcode and delete originals
|
||||
$(basename "$0") ./media/videos --crf 20 # Higher quality
|
||||
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
FOLDER=""
|
||||
REPLACE=false
|
||||
DRY_RUN=false
|
||||
CRF=$DEFAULT_CRF
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--replace)
|
||||
REPLACE=true
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
;;
|
||||
--crf)
|
||||
CRF="$2"
|
||||
shift 2
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
;;
|
||||
-*)
|
||||
echo "Unknown option: $1"
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$FOLDER" ]]; then
|
||||
FOLDER="$1"
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate arguments
|
||||
if [[ -z "$FOLDER" ]]; then
|
||||
echo "Error: Folder path is required"
|
||||
echo ""
|
||||
usage
|
||||
fi
|
||||
|
||||
if [[ ! -d "$FOLDER" && ! -f "$FOLDER" ]]; then
|
||||
echo "Error: Path not found: $FOLDER"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for ffmpeg
|
||||
if ! command -v ffmpeg &> /dev/null; then
|
||||
echo "Error: ffmpeg is not installed"
|
||||
echo "Install with: brew install ffmpeg"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build find pattern for video files
|
||||
build_find_pattern() {
|
||||
local pattern=""
|
||||
for ext in "${EXTENSIONS[@]}"; do
|
||||
if [[ -n "$pattern" ]]; then
|
||||
pattern="$pattern -o"
|
||||
fi
|
||||
pattern="$pattern -iname '*.$ext'"
|
||||
done
|
||||
echo "$pattern"
|
||||
}
|
||||
|
||||
# Format file size for display
|
||||
format_size() {
|
||||
local bytes=$1
|
||||
if (( bytes >= 1073741824 )); then
|
||||
printf "%.1fG" $(echo "scale=1; $bytes / 1073741824" | bc)
|
||||
elif (( bytes >= 1048576 )); then
|
||||
printf "%.1fM" $(echo "scale=1; $bytes / 1048576" | bc)
|
||||
else
|
||||
printf "%.1fK" $(echo "scale=1; $bytes / 1024" | bc)
|
||||
fi
|
||||
}
|
||||
|
||||
# Get file size in bytes
|
||||
get_size() {
|
||||
stat -f%z "$1" 2>/dev/null || echo 0
|
||||
}
|
||||
|
||||
echo "========================================"
|
||||
echo "Video Transcoder"
|
||||
echo "========================================"
|
||||
echo "Folder: $FOLDER"
|
||||
echo "Codec: H.265/HEVC"
|
||||
echo "Resolution: 1080p (scaled down)"
|
||||
echo "Quality: CRF $CRF"
|
||||
echo "Replace: $REPLACE"
|
||||
[[ "$DRY_RUN" == true ]] && echo "DRY RUN: Yes"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# Check if input is a file or folder
|
||||
IS_SINGLE_FILE=false
|
||||
if [[ -f "$FOLDER" ]]; then
|
||||
IS_SINGLE_FILE=true
|
||||
VIDEO_FILES=("$FOLDER")
|
||||
echo "Processing single file"
|
||||
echo ""
|
||||
else
|
||||
# Find all video files (excluding already compressed ones), sorted by size (smallest first)
|
||||
FIND_PATTERN=$(build_find_pattern)
|
||||
|
||||
# Use Python for robust sorting by size (handles spaces in paths correctly)
|
||||
VIDEO_FILES=()
|
||||
while IFS= read -r file; do
|
||||
VIDEO_FILES+=("$file")
|
||||
done < <(eval "find \"$FOLDER\" -type f \( $FIND_PATTERN \)" 2>/dev/null | python3 -c "
|
||||
import sys
|
||||
import os
|
||||
|
||||
files = []
|
||||
for line in sys.stdin:
|
||||
path = line.rstrip('\n')
|
||||
if '_compressed.' in path:
|
||||
continue
|
||||
try:
|
||||
size = os.path.getsize(path)
|
||||
files.append((size, path))
|
||||
except:
|
||||
pass
|
||||
|
||||
files.sort(key=lambda x: x[0])
|
||||
for size, path in files:
|
||||
print(path)
|
||||
")
|
||||
|
||||
if [[ ${#VIDEO_FILES[@]} -eq 0 ]]; then
|
||||
echo "No video files found in $FOLDER"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found ${#VIDEO_FILES[@]} video file(s) to process (smallest first)"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Track totals
|
||||
TOTAL_ORIGINAL=0
|
||||
TOTAL_COMPRESSED=0
|
||||
SUCCESS_COUNT=0
|
||||
FAIL_COUNT=0
|
||||
|
||||
# Process each file
|
||||
for input_file in "${VIDEO_FILES[@]}"; do
|
||||
# Generate output filename
|
||||
dir=$(dirname "$input_file")
|
||||
basename=$(basename "$input_file")
|
||||
name="${basename%.*}"
|
||||
output_file="$dir/${name}_compressed.mp4"
|
||||
|
||||
# Get original size
|
||||
original_size=$(get_size "$input_file")
|
||||
original_size_fmt=$(format_size $original_size)
|
||||
|
||||
echo "----------------------------------------"
|
||||
echo "Input: $input_file ($original_size_fmt)"
|
||||
echo "Output: $output_file"
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
echo "Action: [DRY RUN] Would transcode"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Skip if output already exists
|
||||
if [[ -f "$output_file" ]]; then
|
||||
echo "Action: Skipped (output already exists)"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Transcode with ffmpeg
|
||||
# -vf scale=-2:1080 = scale to 1080p height, auto width (divisible by 2)
|
||||
# -c:v libx265 = H.265/HEVC codec
|
||||
# -crf = quality (lower = better)
|
||||
# -preset medium = encoding speed/compression tradeoff
|
||||
# -c:a aac -b:a 128k = audio to AAC at 128kbps
|
||||
# -tag:v hvc1 = compatibility tag for Apple devices
|
||||
echo "Action: Transcoding..."
|
||||
|
||||
if ffmpeg -i "$input_file" \
|
||||
-vf "scale=-2:1080" \
|
||||
-c:v libx265 \
|
||||
-crf "$CRF" \
|
||||
-preset medium \
|
||||
-c:a aac -b:a 128k \
|
||||
-tag:v hvc1 \
|
||||
-y \
|
||||
"$output_file" \
|
||||
-loglevel warning -stats 2>&1; then
|
||||
|
||||
# Get compressed size
|
||||
compressed_size=$(get_size "$output_file")
|
||||
compressed_size_fmt=$(format_size $compressed_size)
|
||||
|
||||
# Calculate reduction
|
||||
if (( original_size > 0 )); then
|
||||
reduction=$(echo "scale=1; 100 - ($compressed_size * 100 / $original_size)" | bc)
|
||||
else
|
||||
reduction=0
|
||||
fi
|
||||
|
||||
echo "Result: $original_size_fmt → $compressed_size_fmt (${reduction}% reduction)"
|
||||
|
||||
TOTAL_ORIGINAL=$((TOTAL_ORIGINAL + original_size))
|
||||
TOTAL_COMPRESSED=$((TOTAL_COMPRESSED + compressed_size))
|
||||
((SUCCESS_COUNT++))
|
||||
|
||||
# Delete original if --replace is set
|
||||
if [[ "$REPLACE" == true ]]; then
|
||||
rm "$input_file"
|
||||
echo "Deleted: $input_file"
|
||||
fi
|
||||
else
|
||||
echo "Result: FAILED"
|
||||
((FAIL_COUNT++))
|
||||
# Remove partial output file if it exists
|
||||
[[ -f "$output_file" ]] && rm "$output_file"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "Summary"
|
||||
echo "========================================"
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
echo "DRY RUN - no files were transcoded"
|
||||
else
|
||||
echo "Processed: $SUCCESS_COUNT succeeded, $FAIL_COUNT failed"
|
||||
if (( SUCCESS_COUNT > 0 )); then
|
||||
total_orig_fmt=$(format_size $TOTAL_ORIGINAL)
|
||||
total_comp_fmt=$(format_size $TOTAL_COMPRESSED)
|
||||
if (( TOTAL_ORIGINAL > 0 )); then
|
||||
total_reduction=$(echo "scale=1; 100 - ($TOTAL_COMPRESSED * 100 / $TOTAL_ORIGINAL)" | bc)
|
||||
else
|
||||
total_reduction=0
|
||||
fi
|
||||
echo "Total: $total_orig_fmt → $total_comp_fmt (${total_reduction}% reduction)"
|
||||
|
||||
if [[ "$REPLACE" == true ]]; then
|
||||
saved=$(format_size $((TOTAL_ORIGINAL - TOTAL_COMPRESSED)))
|
||||
echo "Freed: $saved"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
echo "========================================"
|
||||
Reference in New Issue
Block a user