| 1 | #!/usr/bin/env bash |
| 2 | |
| 3 | ######################################################################## |
| 4 | # |
| 5 | # This bash script was written by ChatGPT, but not this docblock. |
| 6 | # Everything works after testing, but not 2 functions: |
| 7 | # - Adding metadata to compressed MP4 file. |
| 8 | # - Checking if the file has already been compressed by this script. |
| 9 | # |
| 10 | ######################################################################## |
| 11 | |
| 12 | set -u |
| 13 | |
| 14 | SCRIPT_NAME="$(basename "$0")" |
| 15 | UNIQUE_PREFIX="r-" |
| 16 | |
| 17 | ######################################## |
| 18 | # Encoding presets overview: |
| 19 | # |
| 20 | # none : balanced quality, AAC 96k |
| 21 | # vlog : spoken voice / crowd, copy audio |
| 22 | # tiny : max compression, AAC 64k |
| 23 | # |
| 24 | # Resolutions: 720p | 1080p |
| 25 | ######################################## |
| 26 | |
| 27 | show_help() { |
| 28 | cat <<EOF |
| 29 | Usage: |
| 30 | $SCRIPT_NAME <file> |
| 31 | $SCRIPT_NAME [none|vlog|tiny] [720p|1080p] <file> |
| 32 | $SCRIPT_NAME batch |
| 33 | |
| 34 | Batch: |
| 35 | Compresses all MP4 + MKV files using: |
| 36 | mode=none, resolution=1080p |
| 37 | |
| 38 | Defaults: |
| 39 | mode=none |
| 40 | resolution=720p |
| 41 | |
| 42 | EOF |
| 43 | exit 0 |
| 44 | } |
| 45 | |
| 46 | [[ "${1:-}" == "--help" ]] && show_help |
| 47 | |
| 48 | MODE="none" |
| 49 | RES="720p" |
| 50 | |
| 51 | # --- Batch mode --- |
| 52 | if [[ "${1:-}" == "batch" ]]; then |
| 53 | MODE="none" |
| 54 | RES="1080p" |
| 55 | shift || true |
| 56 | |
| 57 | shopt -s nullglob |
| 58 | FILES=( *.mp4 *.mkv ) |
| 59 | shopt -u nullglob |
| 60 | |
| 61 | if [[ ${#FILES[@]} -eq 0 ]]; then |
| 62 | echo "No MP4 or MKV files found." |
| 63 | exit 0 |
| 64 | fi |
| 65 | |
| 66 | for f in "${FILES[@]}"; do |
| 67 | "$0" "$MODE" "$RES" "$f" |
| 68 | done |
| 69 | exit 0 |
| 70 | fi |
| 71 | |
| 72 | # --- Parse arguments --- |
| 73 | if [[ $# -eq 1 ]]; then |
| 74 | INPUT="$1" |
| 75 | elif [[ $# -eq 2 ]]; then |
| 76 | MODE="$1" |
| 77 | INPUT="$2" |
| 78 | elif [[ $# -eq 3 ]]; then |
| 79 | MODE="$1" |
| 80 | RES="$2" |
| 81 | INPUT="$3" |
| 82 | else |
| 83 | show_help |
| 84 | fi |
| 85 | |
| 86 | [[ ! -f "$INPUT" ]] && { echo "File not found: $INPUT"; exit 1; } |
| 87 | |
| 88 | BASENAME="${INPUT%.*}" |
| 89 | OUTPUT="${BASENAME}-r.mp4" |
| 90 | SUMMARY="${BASENAME}-summary.json" |
| 91 | |
| 92 | ######################################## |
| 93 | # Resolution |
| 94 | ######################################## |
| 95 | case "$RES" in |
| 96 | 720p) SCALE="scale=-2:720" ;; |
| 97 | 1080p) SCALE="scale=-2:1080" ;; |
| 98 | *) echo "Invalid resolution"; exit 1 ;; |
| 99 | esac |
| 100 | |
| 101 | ######################################## |
| 102 | # Mode settings |
| 103 | ######################################## |
| 104 | CQ=23 |
| 105 | AUDIO_OPTS=("-c:a" "aac" "-b:a" "96k") |
| 106 | |
| 107 | case "$MODE" in |
| 108 | vlog) |
| 109 | AUDIO_OPTS=("-c:a" "copy") |
| 110 | CQ=23 |
| 111 | ;; |
| 112 | tiny) |
| 113 | AUDIO_OPTS=("-c:a" "aac" "-b:a" "64k") |
| 114 | CQ=28 |
| 115 | ;; |
| 116 | none) ;; |
| 117 | *) |
| 118 | echo "Invalid mode" |
| 119 | exit 1 |
| 120 | ;; |
| 121 | esac |
| 122 | |
| 123 | ######################################## |
| 124 | # Metadata detection (robust) |
| 125 | ######################################## |
| 126 | EXISTING_ID="$(ffprobe -v error \ |
| 127 | -show_entries format_tags=compressed_by \ |
| 128 | -of default=nk=1:nw=1 "$INPUT" 2>/dev/null || true)" |
| 129 | |
| 130 | if [[ "$EXISTING_ID" =~ ^${UNIQUE_PREFIX}[A-Za-z0-9]+$ ]]; then |
| 131 | echo "Skipping already-compressed file: $INPUT" |
| 132 | exit 0 |
| 133 | fi |
| 134 | |
| 135 | ######################################## |
| 136 | # Generate unique ID |
| 137 | ######################################## |
| 138 | COMPRESS_ID="${UNIQUE_PREFIX}$(tr -dc A-Za-z0-9 </dev/urandom | head -c 12)" |
| 139 | |
| 140 | ######################################## |
| 141 | # Encode |
| 142 | ######################################## |
| 143 | echo "Compressing: $INPUT > $OUTPUT" |
| 144 | |
| 145 | if ! ffmpeg -y -hide_banner -loglevel error -stats \ |
| 146 | -i "$INPUT" \ |
| 147 | -map 0:v:0 -map 0:a? \ |
| 148 | -vf "$SCALE" \ |
| 149 | -c:v hevc_nvenc \ |
| 150 | -preset p6 -tune hq \ |
| 151 | -rc vbr -multipass fullres \ |
| 152 | -profile:v main -pix_fmt yuv420p \ |
| 153 | -spatial_aq 1 -temporal_aq 1 -aq-strength 8 \ |
| 154 | -cq "$CQ" \ |
| 155 | "${AUDIO_OPTS[@]}" \ |
| 156 | -movflags +faststart \ |
| 157 | -metadata compressed_by="$COMPRESS_ID" \ |
| 158 | "$OUTPUT"; then |
| 159 | echo "FFmpeg failed for $INPUT" |
| 160 | exit 0 |
| 161 | fi |
| 162 | |
| 163 | ######################################## |
| 164 | # Size check |
| 165 | ######################################## |
| 166 | ORIG_SIZE=$(stat -c%s "$INPUT") |
| 167 | NEW_SIZE=$(stat -c%s "$OUTPUT") |
| 168 | |
| 169 | if (( NEW_SIZE >= ORIG_SIZE )); then |
| 170 | echo "Result larger than original → skipping" |
| 171 | rm -f "$OUTPUT" |
| 172 | exit 0 |
| 173 | fi |
| 174 | |
| 175 | ######################################## |
| 176 | # JSON summary |
| 177 | ######################################## |
| 178 | HUMAN_ORIG=$(numfmt --to=iec --suffix=B "$ORIG_SIZE") |
| 179 | HUMAN_NEW=$(numfmt --to=iec --suffix=B "$NEW_SIZE") |
| 180 | OCCURRED=$(date +"%Y-%m-%d %H:%M:%S") |
| 181 | |
| 182 | cat > "$SUMMARY" <<EOF |
| 183 | { |
| 184 | "input": "$(basename "$INPUT")", |
| 185 | "output": "$(basename "$OUTPUT")", |
| 186 | |
| 187 | "original_bytes": $ORIG_SIZE, |
| 188 | "compressed_bytes": $NEW_SIZE, |
| 189 | |
| 190 | "original": "$HUMAN_ORIG", |
| 191 | "compressed": "$HUMAN_NEW", |
| 192 | |
| 193 | "reduction_percent": $(awk "BEGIN { printf \"%.2f\", (1-$NEW_SIZE/$ORIG_SIZE)*100 }"), |
| 194 | "occurred": "$OCCURRED", |
| 195 | |
| 196 | "compression_id": "$COMPRESS_ID", |
| 197 | |
| 198 | "settings": { |
| 199 | "codec": "hevc_nvenc", |
| 200 | "resolution": "$RES", |
| 201 | "mode": "$MODE", |
| 202 | "cq": $CQ, |
| 203 | "scale_filter": "$SCALE", |
| 204 | "audio": "${AUDIO_OPTS[*]}", |
| 205 | "preset": "p6", |
| 206 | "rate_control": "vbr", |
| 207 | "multipass": "fullres", |
| 208 | "pixel_format": "yuv420p", |
| 209 | "aq": { |
| 210 | "spatial": 1, |
| 211 | "temporal": 1, |
| 212 | "strength": 8 |
| 213 | } |
| 214 | } |
| 215 | } |
| 216 | EOF |
| 217 | |
| 218 | echo "Done ✓" |
airikr / Compress MKV and MP4 to MP4 with options
Last active 4 months ago
Created by ChatGPT, but not the first comment section.
Revision 01229c97abaca94cb84e61ba7db5c8243f22a3be