Files
fish/functions/vrot.fish
T
2026-08-14 13:39:41 +02:00

320 lines
11 KiB
Fish
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
function vrot -d 'Rotate a video with ffmpeg: metadata-only by default, re-encode with -r'
argparse -n vrot 'h/help' 'r/reencode' 'a/angle=' 'p/preset=' -- $argv
or return 2
if set -q _flag_help
__vrot_usage
return 0
end
if test (count $argv) -lt 1
__vrot_usage
return 2
end
set -q _flag_angle; or set _flag_angle 90
set -q _flag_preset; or set _flag_preset medium
switch $_flag_angle
case 90 180 270
case '*'
__vrot_error "angle must be 90, 180 or 270 (got '$_flag_angle')"
return 2
end
__vrot_colors
set -l want_reencode 0
set -q _flag_reencode; and set want_reencode 1
set -l rc 0
for file in $argv
if not test -f "$file"
__vrot_error "no such file: '$file'"
set rc 1
continue
end
__vrot_one "$file" "$_flag_angle" "$_flag_preset" "$want_reencode"
or set rc 1
end
return $rc
end
function __vrot_one
set -l file $argv[1]
set -l angle $argv[2]
set -l preset $argv[3]
set -l want_reencode $argv[4]
set -l name (path basename "$file")
set -l ext (path extension "$file")
set -l out (path change-extension '' "$file")_rot$ext
set -l outname (path basename "$out")
__vrot_banner "$name$outname"
set -l lines (ffprobe -v error -select_streams v:0 \
-show_entries stream=codec_name,width,height,r_frame_rate,nb_frames:format=duration,bit_rate \
-of default=noprint_wrappers=1 "$file")
set -l codec (string match 'codec_name=*' -- $lines | string replace 'codec_name=' '')[1]
set -l width (string match 'width=*' -- $lines | string replace 'width=' '')[1]
set -l height (string match 'height=*' -- $lines | string replace 'height=' '')[1]
set -l fpsraw (string match 'r_frame_rate=*' -- $lines | string replace 'r_frame_rate=' '')[1]
set -l frames (string match 'nb_frames=*' -- $lines | string replace 'nb_frames=' '')[1]
set -l duration (string match 'duration=*' -- $lines | string replace 'duration=' '')[1]
set -l bitrate (string match 'bit_rate=*' -- $lines | string replace 'bit_rate=' '')[1]
set -l info "$width"x"$height"
if test -n "$codec"
set info "$info "(string upper "$codec")
end
if test -n "$fpsraw"; and string match -rq '^[0-9]+/[0-9]+$' "$fpsraw"; and not string match -q '*/0' "$fpsraw"
set info "$info "(math -s 2 "$fpsraw")" fps"
end
if test -n "$duration"; and string match -rq '^[0-9.]+$' "$duration"
set info "$info "(__vrot_fmtsecs "$duration")
end
if test -n "$bitrate"; and string match -rq '^[0-9.]+$' "$bitrate"
set info "$info "(math -s 1 "$bitrate/1000000")" Mbit/s"
end
echo "$vrot_c_dim $info$vrot_c_reset"
if test "$file" = "$out"
__vrot_error "output would overwrite input: '$out'"
return 1
end
if test -e "$out"
read -l -P "vrot: '$out' exists. Overwrite? [y/N] " ans
if not string match -qi 'y' "$ans"
echo "$vrot_c_dim skipped: $out$vrot_c_reset"
return 1
end
rm -f "$out"
end
if test "$want_reencode" -eq 1
__vrot_reencode "$file" "$out" "$angle" "$preset" "$height" "$codec" "$duration" "$frames"
return $status
end
echo "$vrot_c_dim mode: metadata-only rotation (lossless, instant)$vrot_c_reset"
ffmpeg -hide_banner -loglevel error -nostdin -i "$file" -map 0 -c copy -ignore_unknown \
-movflags +faststart -metadata:s:v:0 rotate=$angle "$out"
if test $status -eq 0
__vrot_summary "$out"
return 0
end
echo "$vrot_c_warn ↷ dropping unsupported data/attachment tracks, retrying…$vrot_c_reset"
rm -f "$out"
ffmpeg -hide_banner -loglevel error -nostdin -i "$file" -map 0 -map '-0:d' -map '-0:t' \
-c copy -movflags +faststart -metadata:s:v:0 rotate=$angle "$out"
if test $status -eq 0
__vrot_summary "$out"
return 0
end
echo "$vrot_c_warn ↷ metadata rotation unsupported here, re-encoding instead…$vrot_c_reset"
rm -f "$out"
__vrot_reencode "$file" "$out" "$angle" "$preset" "$height" "$codec" "$duration" "$frames"
end
function __vrot_reencode
set -l file $argv[1]
set -l out $argv[2]
set -l angle $argv[3]
set -l preset $argv[4]
set -l height $argv[5]
set -l codec $argv[6]
set -l duration $argv[7]
set -l frames $argv[8]
test -n "$height"; or set height 1080
set -l enc libx264
switch "$codec"
case 'hevc*' 'h265*' 'h266*'
set enc libx265
end
set -l crf 20
set -l maxrate 20M
set -l bufsize 40M
if test $height -ge 2160
set crf 22; set maxrate 40M; set bufsize 80M
else if test $height -ge 1440
set crf 21; set maxrate 30M; set bufsize 60M
else if test $height -ge 1080
set crf 20; set maxrate 20M; set bufsize 40M
else if test $height -ge 720
set crf 19; set maxrate 10M; set bufsize 20M
else
set crf 18; set maxrate 6M; set bufsize 12M
end
set -l vf 'transpose=1'
switch $angle
case 180
set vf 'transpose=1,transpose=1'
case 270
set vf 'transpose=2'
end
set -l faststart
switch (string lower (path extension "$out"))
case '.mp4' '.mov' '.m4v'
set faststart '-movflags' '+faststart'
end
echo "$vrot_c_dim mode: re-encode ($enc · crf $crf · cap $maxrate)$vrot_c_reset"
set -l total_frames ''
string match -rq '^\d+$' "$frames"; and test "$frames" -gt 0; and set total_frames $frames
set -l out_t ''
set -l frame 0
set -l fps_txt ''
set -l bitrate_txt ''
set -l speed_txt ''
ffmpeg -hide_banner -loglevel error -nostdin -progress pipe:1 -i "$file" \
-map 0:v -map '0:a?' -map '0:s?' -ignore_unknown -vf "$vf" -c:v $enc -crf $crf \
-maxrate $maxrate -bufsize $bufsize -preset $preset -c:a copy -c:s copy \
$faststart "$out" | while read -l line
switch "$line"
case 'frame=*'
set frame (string replace 'frame=' '' -- "$line")
case 'fps=*'
set fps_txt (string replace 'fps=' '' -- "$line")"fps"
case 'bitrate=*'
set bitrate_txt (string replace 'bitrate=' '' -- "$line")
if string match -q '*kbits/s' "$bitrate_txt"
set bitrate_txt (math -s 1 (string replace 'kbits/s' '' -- "$bitrate_txt") / 1000)"Mbit/s"
else if string match -q '*Mbits/s' "$bitrate_txt"
set bitrate_txt (string replace 'Mbits/s' 'Mbit/s' -- "$bitrate_txt")
end
case 'out_time=*'
set -l t (string replace 'out_time=' '' -- "$line")
set -l parts (string split ':' -- "$t")
if test (count $parts) -eq 3; and string match -rq '^\d+$' "$parts[1]"; and string match -rq '^\d+$' "$parts[2]"; and string match -rq '^[\d.]+$' "$parts[3]"
set out_t (math "$parts[1]*3600+$parts[2]*60+$parts[3]")
end
case 'speed=*'
set speed_txt (string replace 'speed=' '' -- "$line")
end
if isatty stdout
printf '\r\033[K'
set -l pct '--.-%'
set -l eta_txt ''
set -l bar (string repeat -n 24 '░')
set -l frac ''
if test -n "$total_frames"; and string match -rq '^\d+$' "$frame"; and test "$total_frames" -gt 0
set frac (math "min(1, $frame/$total_frames)")
else if test -n "$out_t"; and string match -rq '^[0-9.]+$' "$duration"; and not string match -rq '^0+(\.0*)?$' "$duration"
set frac (math "min(1, $out_t/$duration)")
end
if test -n "$frac"
set -l filled (math "floor($frac*24)")
set -l bar (string repeat -n $filled '█')
set bar "$bar"(string repeat -n (math "24-$filled") '░')
set pct (math -s 1 "$frac*100")%
set -l sp (string replace -r '[xX]$' '' -- "$speed_txt")
if test -n "$sp"; and string match -rq '^[0-9.]+$' "$sp"; and not string match -rq '^0+(\.0*)?$' "$sp"; and test -n "$out_t"; and string match -rq '^[0-9.]+$'
"$duration"
set eta_txt " ETA "(__vrot_fmtsecs (math "($duration - $out_t)/$sp"))
end
end
set -l frame_txt $frame
if test -n "$total_frames"
set frame_txt "$frame/$total_frames"
end
echo -n " $vrot_c_ok$bar$vrot_c_reset $pct frame=$frame_txt $fps_txt $bitrate_txt $speed_txt$eta_txt"
end
end
set -l ff_status $pipestatus[1]
printf '\n'
if test $ff_status -ne 0
__vrot_error "encoding failed (ffmpeg exited $ff_status)"
return 1
end
__vrot_summary "$out"
end
function __vrot_summary
set -l out $argv[1]
set -l size ''
if command -q numfmt
set size (numfmt --to=iec --suffix=B -- (stat -c %s -- "$out") 2>/dev/null)
else
set size (du -h -- "$out" | string replace -r '\t.*' '')
end
set -l dims (ffprobe -v error -select_streams v:0 \
-show_entries stream=width,height -of default=noprint_wrappers=1:nokey=1 "$out" \
| string match -r '^\d+$' | string join '×')
set -l extra ''
if test -n "$dims"
set extra " · $dims"
end
echo "$vrot_c_ok$out$vrot_c_dim ($size$extra)$vrot_c_reset"
end
function __vrot_banner
set -l title $argv[1]
echo "$vrot_c_dim"(string repeat -n 58 '─')"$vrot_c_reset"
echo "$vrot_c_hi $title$vrot_c_reset"
end
function __vrot_colors
set -q vrot_colors_set; and return 0
set -g vrot_colors_set 1
if isatty stdout
set -g vrot_c_hi (set_color brwhite --bold)
set -g vrot_c_dim (set_color brblack)
set -g vrot_c_ok (set_color brgreen)
set -g vrot_c_warn (set_color bryellow)
set -g vrot_c_err (set_color brred)
set -g vrot_c_reset (set_color normal)
else
for c in hi dim ok warn err reset
set -g vrot_c_$c ''
end
end
end
function __vrot_fmtsecs
set -l s (math "max(0, round($argv[1]))")
set -l h (math "floor($s/3600)")
set -l m (math "floor($s%3600/60)")
set -l sec (math "$s%60")
if test $h -gt 0
printf '%02d:%02d:%02d' $h $m $sec
else
printf '%02d:%02d' $m $sec
end
end
function __vrot_error
echo "$vrot_c_err""vrot: "$argv"$vrot_c_reset" >&2
end
function __vrot_usage
echo 'usage: vrot [options] <video> [<video> ...]'
echo
echo 'Rotates one or more videos and writes <name>_rot.<ext> next to each.'
echo
echo 'Default mode is metadata-only rotation: lossless and instant. Works for'
echo 'MP4/MOV/M4V (including GoPro files; unsupported data/timecode tracks are'
echo 'dropped automatically). If the container cannot carry the flag, vrot'
echo 'falls back to re-encoding, with a live progress bar. Players that'
echo 'ignore rotation flags need -r.'
echo
echo 'options:'
echo ' -a, --angle <90|180|270> clockwise angle (default 90)'
echo ' -r, --reencode rotate pixels by re-encoding'
echo ' -p, --preset <name> x264/x265 preset (default medium)'
echo ' -h, --help show this help'
end