aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xtranscode.sh203
1 files changed, 121 insertions, 82 deletions
diff --git a/transcode.sh b/transcode.sh
index 0501974..08ed14e 100755
--- a/transcode.sh
+++ b/transcode.sh
@@ -148,7 +148,7 @@ declare -A SKIP_CODECS
REMAINING_ARGS=()
-TMP_FILES=()
+TEMPORARY_FILES=()
readonly PRESET_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/transcode.sh/presets"
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/transcode.sh/config.toml"
@@ -472,47 +472,65 @@ load_config() {
# [encoding] preset
if [[ $_CLI_PRESET -eq 0 ]]; then
- val=$(_tomlq_get '.encoding.preset // empty') && [[ -n "$val" ]] && {
+ val=$(_tomlq_get '.encoding.preset // empty')
+ if [[ -n "$val" ]]; then
debug "Config: preset =" "$val"
PRESET_NAME="$val"
- }
+ fi
fi
# [encoding] nice
if [[ $_CLI_NICE -eq 0 ]]; then
- val=$(_tomlq_get '.encoding.nice // empty') && [[ -n "$val" ]] && {
+ val=$(_tomlq_get '.encoding.nice // empty')
+ if [[ -n "$val" ]]; then
debug "Config: nice =" "$val"
NICE_VALUE="$val"
- }
+ fi
fi
# [encoding] only_if_smaller
if [[ $_CLI_ONLY_IF_SMALLER -eq 0 ]]; then
- val=$(_tomlq_get '.encoding.only_if_smaller // empty') && [[ -n "$val" ]] && {
+ val=$(_tomlq_get '.encoding.only_if_smaller // empty')
+ if [[ -n "$val" ]]; then
debug "Config: encoding.only_if_smaller =" "$val"
- [[ "$val" == "true" ]] && ONLY_IF_SMALLER=1 || ONLY_IF_SMALLER=0
- }
+ if [[ "$val" == "true" ]]; then
+ ONLY_IF_SMALLER=1
+ else
+ ONLY_IF_SMALLER=0
+ fi
+ fi
fi
# [encoding] continue
if [[ $_CLI_CONTINUE -eq 0 ]]; then
- val=$(_tomlq_get '.encoding.continue // empty') && [[ -n "$val" ]] && {
+ val=$(_tomlq_get '.encoding.continue // empty')
+ if [[ -n "$val" ]]; then
debug "Config: encoding.continue =" "$val"
- [[ "$val" == "true" ]] && CONTINUE_ON_FAIL=1 || CONTINUE_ON_FAIL=0
- }
+ if [[ "$val" == "true" ]]; then
+ CONTINUE_ON_FAIL=1
+ else
+ CONTINUE_ON_FAIL=0
+ fi
+ fi
fi
# [encoding] verify_output
if [[ $_CLI_VERIFY_OUTPUT -eq 0 ]]; then
- val=$(_tomlq_get '.encoding.verify_output // empty') && [[ -n "$val" ]] && {
+ val=$(_tomlq_get '.encoding.verify_output // empty')
+ if [[ -n "$val" ]]; then
debug "Config: encoding.verify_output =" "$val"
- [[ "$val" == "true" ]] && VERIFY_OUTPUT=1 || VERIFY_OUTPUT=0
- }
+ if [[ "$val" == "true" ]]; then
+ VERIFY_OUTPUT=1
+ else
+ VERIFY_OUTPUT=0
+ fi
+ fi
fi
# [encoding] hwaccel
if [[ $_CLI_HWACCEL -eq 0 ]]; then
- val=$(_tomlq_get '.encoding.hwaccel // empty') && [[ -n "$val" ]] && {
+ val=$(_tomlq_get '.encoding.hwaccel // empty')
+ if [[ -n "$val" ]]; then
debug "Config: encoding.hwaccel =" "$val"
if [[ "$val" == "false" ]]; then
HWACCEL=0
@@ -523,7 +541,7 @@ load_config() {
HWACCEL_VALUE="$val"
fi
fi
- }
+ fi
fi
# [skip] codecs (TOML array → one element per line via tomlq -r '.skip.codecs[]')
@@ -531,7 +549,9 @@ load_config() {
# regardless of _CLI_SKIP_CODEC. Both sources merge into SKIP_CODECS.
local codec
while IFS= read -r codec; do
- [[ -n "$codec" ]] && add_skip_codec_token "$codec"
+ if [[ -n "$codec" ]]; then
+ add_skip_codec_token "$codec"
+ fi
done < <(tomlq -r '.skip.codecs[]?' "$CONFIG_FILE" 2>/dev/null)
# [saving] enabled
@@ -552,25 +572,36 @@ load_config() {
# [output] verbose
if [[ $_CLI_VERBOSE -eq 0 ]]; then
- val=$(_tomlq_get '.output.verbose // empty') && [[ -n "$val" ]] && {
+ val=$(_tomlq_get '.output.verbose // empty')
+ if [[ -n "$val" ]]; then
debug "Config: output.verbose =" "$val"
- [[ "$val" == "true" ]] && VERBOSE=1 || VERBOSE=0
- }
+ if [[ "$val" == "true" ]]; then
+ VERBOSE=1
+ else
+ VERBOSE=0
+ fi
+ fi
fi
# [output] quiet
if [[ $_CLI_QUIET -eq 0 ]]; then
- val=$(_tomlq_get '.output.quiet // empty') && [[ -n "$val" ]] && {
+ val=$(_tomlq_get '.output.quiet // empty')
+ if [[ -n "$val" ]]; then
debug "Config: output.quiet =" "$val"
- [[ "$val" == "true" ]] && QUIET=1 || QUIET=0
- }
+ if [[ "$val" == "true" ]]; then
+ QUIET=1
+ else
+ QUIET=0
+ fi
+ fi
fi
}
# Add a codec name to the SKIP_CODECS associative array.
# - Normalizes to lowercase
# - Ignores empty tokens
-# - Used both for CLI --skip-codec and for skip.conf lines
+# - Used both for CLI --skip-codec and for codecs defined
+# in the config file
add_skip_codec_token() {
local token="${1,,}" # lowercase
if [[ ! -v SKIP_CODECS["$token"] ]]; then
@@ -587,8 +618,9 @@ parse_skip_codec_parameter() {
local arg="$1"
local IFS=',: ' token
for token in $arg; do
- [[ -z "$token" ]] && continue
- add_skip_codec_token "$token"
+ if [[ ! -z "$token" ]]; then
+ add_skip_codec_token "$token"
+ fi
done
}
@@ -694,7 +726,7 @@ list_presets() {
local canonical_preset_dir
canonical_preset_dir=$(resolve_path "$PRESET_DIR")
- printf "${BOLD}${BLUE}Available presets in${ALL_OFF} ${BOLD}${CYAN}%s${ALL_OFF}${BOLD}${BLUE}:${ALL_OFF}\n" "$PRESET_DIR"
+ msg "Available presets in:" "$PRESET_DIR"
local target_path target_name description
for f in "${preset_files[@]}"; do
@@ -706,16 +738,16 @@ list_presets() {
if [[ "$target_path" == "$canonical_preset_dir"/*.sh ]]; then
target_name="${target_path##*/}"
target_name="${target_name%.sh}"
- printf " ${BOLD}${GREEN}%-${max_name_len}s${ALL_OFF} ${BOLD}${YELLOW}-${ALL_OFF} Symlinked to ${BOLD}${GREEN}%s${ALL_OFF}\n" "$name" "$target_name"
+ printf "${BOLD}${BLUE}%-${max_name_len}s${ALL_OFF} ${BOLD}${YELLOW}-${ALL_OFF} Symlinked to ${BOLD}${BLUE}%s${ALL_OFF}\n" "$name" "$target_name"
continue
fi
fi
description=$(sed -n '1s/^[[:space:]]*#[[:space:]]*[Dd]escription:[[:space:]]*//p' "$f")
if [[ -n "$description" ]]; then
- printf " ${BOLD}${GREEN}%-${max_name_len}s${ALL_OFF} ${BOLD}${YELLOW}-${ALL_OFF} %s\n" "$name" "$description"
+ printf "${BOLD}${BLUE}%-${max_name_len}s${ALL_OFF} ${BOLD}${YELLOW}-${ALL_OFF} %s\n" "$name" "$description"
else
- printf " ${BOLD}${GREEN}%-${max_name_len}s${ALL_OFF}\n" "$name"
+ printf "${BOLD}${BLUE}%-${max_name_len}s${ALL_OFF}\n" "$name"
fi
done
}
@@ -1023,9 +1055,11 @@ unset joined
cleanup() {
# "${TMP_FILES[@]+"${TMP_FILES[@]}"}" expands to nothing when the array is
# empty (safe under nounset), unlike [@]:-} which yields one empty iteration.
- for t in "${TMP_FILES[@]+"${TMP_FILES[@]}"}"; do
+ for t in "${TEMPORARY_FILES[@]+"${TEMPORARY_FILES[@]}"}"; do
debug "Cleanup temp file:" "$t"
- [[ -f "$t" ]] && rm -f -- "$t" || true
+ if [[ -f "$t" ]]; then
+ rm -f -- "$t"
+ fi
done
}
trap cleanup EXIT
@@ -1049,28 +1083,30 @@ trap cleanup EXIT
# Return:
# EXIT_OK on success or if skipped; EXIT_RUNTIME_FAILURE on failure.
encode_one() {
- local file="$1"
- local idx="$2"
- local total="$3"
+ local input_file="$1"
+ local current_file_index="$2"
+ local amount_total_files="$3"
- [[ -z "$file" ]] && return $EXIT_OK
+ if [[ -z "$input_file" ]]; then
+ return $EXIT_OK
+ fi
- msg "Processing [$idx/$total]:" "$file"
- [[ ! -f "$file" ]] && {
- error "Not found:" "$file"
+ msg "Processing [$current_file_index/$amount_total_files]:" "$input_file"
+ if [[ ! -f "$input_file" ]]; then
+ error "Not found:" "$input_file"
return $EXIT_RUNTIME_FAILURE
- }
- [[ ! -r "$file" ]] && {
- error "File is not readable:" "$file"
+ fi
+ if [[ ! -r "$input_file" ]]; then
+ error "File is not readable:" "$input_file"
return $EXIT_RUNTIME_FAILURE
- }
+ fi
# Probe codec and pixel format in a single ffprobe invocation to halve
# the startup overhead on large batches.
local probe_out
probe_out=$(ffprobe -v error -select_streams v:0 \
-show_entries stream=codec_name,pix_fmt,r_frame_rate \
- -of default=nk=1:nw=1 "file:$file" 2>/dev/null)
+ -of default=nk=1:nw=1 "file:$input_file" 2>/dev/null)
local input_codec input_pixel_format input_frame_rate input_fps
@@ -1081,15 +1117,15 @@ encode_one() {
input_frame_rate=$(printf "%s\n" "$probe_out" | sed -n '3p')
if [[ -z "$input_codec" ]]; then
- error "Could not determine input video codec:" "$file"
+ error "Could not determine input video codec:" "$input_file"
return $EXIT_RUNTIME_FAILURE
fi
if [[ -z "$input_pixel_format" ]]; then
- error "Could not determine input pixel format:" "$file"
+ error "Could not determine input pixel format:" "$input_file"
return $EXIT_RUNTIME_FAILURE
fi
if [[ -z "$input_frame_rate" ]]; then
- error "Could not determine input frame rate:" "$file"
+ error "Could not determine input frame rate:" "$input_file"
return $EXIT_RUNTIME_FAILURE
fi
@@ -1114,8 +1150,9 @@ encode_one() {
*422*) subsampling="422" ;;
*)
subsampling="420"
- [[ "$input_pixel_format" != *420* ]] &&
+ if [[ "$input_pixel_format" != *420* ]]; then
verbose "Unrecognised chroma subsampling in '$input_pixel_format', falling back to yuv420p"
+ fi
;;
esac
@@ -1153,32 +1190,32 @@ encode_one() {
load_preset "$PRESET_NAME"
- local extension directory tmp base stem
+ local extension directory temporary_output_file base stem
# filename (path without extension) is used only for the savings log.
# Note: only the last extension is stripped, so multi-dot names like
# "my.show.s01e01.mkv" are logged as "my.show.s01e01".
local filename
- base="${file##*/}"
+ base="${input_file##*/}"
if [[ "$base" == *.* ]]; then
extension="${base##*.}"
stem="${base%.*}"
- filename="${file%.*}"
+ filename="${input_file%.*}"
else
extension=""
stem="$base"
- filename="$file"
+ filename="$input_file"
fi
- directory=$(dirname -- "$file")
+ directory=$(dirname -- "$input_file")
# Temp file is created in the same directory as the input so that `mv` is
# atomic on the same filesystem (avoid cross-device rename issues).
# mktemp provides cryptographically random names and atomic creation,
# unlike $$.$RANDOM which has limited entropy and is predictable.
- tmp=$(mktemp -- "$directory/.${stem}_${PRESET_NAME}.XXXXXX${extension:+.$extension}")
- TMP_FILES+=("$tmp")
- debug "Temporary output path:" "$tmp"
+ temporary_output_file=$(mktemp -- "$directory/.${stem}_${PRESET_NAME}.XXXXXX${extension:+.$extension}")
+ TEMPORARY_FILES+=("$temporary_output_file")
+ debug "Temporary output path:" "$temporary_output_file"
# Build ffmpeg command:
# - Map all streams, metadata, and chapters
@@ -1191,20 +1228,20 @@ encode_one() {
hwaccel_args=(-hwaccel "$HWACCEL_VALUE")
fi
- local cmd=(ffmpeg
+ local encode_cmd=(ffmpeg
-nostdin
-y
-hide_banner
-v error
-stats
"${hwaccel_args[@]}"
- -i "file:$file"
+ -i "file:$input_file"
-map 0
-map_metadata 0
-map_chapters 0
-c copy
"${ffargs[@]}"
- "file:$tmp")
+ "file:$temporary_output_file")
if [[ $DRY_RUN -eq 1 ]]; then
msg "DRY RUN:" "Would encode with preset $PRESET_NAME"
@@ -1214,15 +1251,13 @@ encode_one() {
verbose "Input frame rate:" "$input_frame_rate"
verbose "Input FPS:" "$input_fps"
verbose "Output GOP size:" "$output_gop_size"
- verbose "Command:" "${cmd[*]}"
+ verbose "Command:" "${encode_cmd[*]}"
return $EXIT_OK
fi
- msg "Encoding:" "$file"
- debug "Nice value:" "$NICE_VALUE"
-
- if nice -n "$NICE_VALUE" "${cmd[@]}"; then
+ msg "Encoding:" "$input_file"
+ if nice -n "$NICE_VALUE" "${encode_cmd[@]}"; then
# Integrity verification: confirm the encoded output is a valid, playable
# file before discarding the original. ffprobe exits non-zero and emits
# nothing useful if the container is corrupt or contains no readable
@@ -1230,48 +1265,50 @@ encode_one() {
# Skip with --no-verify-output when trust in the encoder is high and the
# extra probe round-trip is undesirable.
if [[ $VERIFY_OUTPUT -eq 1 ]]; then
- debug "Verifying output integrity:" "$tmp"
- if ! ffprobe -v error "file:$tmp" >/dev/null 2>&1; then
- error "Output failed integrity check:" "$tmp"
- rm -f -- "$tmp"
+ debug "Verifying output integrity:" "$temporary_output_file"
+ if ! ffprobe -v error "file:$temporary_output_file" >/dev/null 2>&1; then
+ error "Output failed integrity check:" "$temporary_output_file"
+ rm -f -- "$temporary_output_file"
if [[ $CONTINUE_ON_FAIL -eq 1 ]]; then
return $EXIT_RUNTIME_FAILURE
else
exit $EXIT_RUNTIME_FAILURE
fi
fi
- verbose "Integrity check passed:" "$tmp"
+ verbose "Integrity check passed:" "$temporary_output_file"
fi
local original_filesize new_filesize
- original_filesize=$(filesize "$file")
- new_filesize=$(filesize "$tmp")
+ original_filesize=$(filesize "$input_file")
+ new_filesize=$(filesize "$temporary_output_file")
# If requested, keep the original if the new file is larger.
if [[ $ONLY_IF_SMALLER -eq 1 && $new_filesize -gt $original_filesize ]]; then
- msg "Did not replace (new file is larger):" "$file"
- rm -f -- "$tmp"
+ msg "Did not replace (new file is larger):" "$input_file"
+ rm -f -- "$temporary_output_file"
return $EXIT_OK
fi
- if [[ $SAVING -eq 1 ]]; then
- local pct
+ local filesize_percentage filesize_difference
+ filesize_difference=$((original_filesize - new_filesize))
+
if [[ $original_filesize -gt 0 ]]; then
- pct=$(((original_filesize - new_filesize) * 100 / original_filesize))
+ filesize_percentage=$((filesize_difference * 100 / original_filesize))
else
- pct=0
+ filesize_percentage=0
fi
# Write a header line if the savings file does not yet exist.
if [[ ! -f "$SAVING_FILE" ]]; then
printf "filename\toriginal_bytes\tnew_bytes\tsaved_pct\n" >"$SAVING_FILE"
+ else
+ printf "%s\t%s\t%s\t%s%%\n" "$filename" "$original_filesize" "$new_filesize" "$filesize_percentage" >>"$SAVING_FILE"
fi
- printf "%s\t%s\t%s\t%s%%\n" "$filename" "$original_filesize" "$new_filesize" "$pct" >>"$SAVING_FILE"
fi
- mv -f -- "$tmp" "$file"
- msg "Replaced:" "$file"
+ mv -f -- "$temporary_output_file" "$input_file"
+ msg "Replaced:" "$input_file"
else
- error "Encode FAILED:" "$file"
+ error "Encode FAILED:" "$input_file"
if [[ $CONTINUE_ON_FAIL -eq 1 ]]; then
return $EXIT_RUNTIME_FAILURE # fail this but keep going
else
@@ -1289,7 +1326,9 @@ if [[ -n $ENCODE_FILE ]]; then
# Build the file list into an array first so we know the total count.
mapfile -t FILES_TO_ENCODE < <(
while IFS= read -r file || [[ -n "$file" ]]; do
- [[ -n "$file" ]] && printf '%s\n' "$file"
+ if [[ -n "$file" ]]; then
+ printf '%s\n' "$file"
+ fi
done <"$ENCODE_FILE"
)
total=${#FILES_TO_ENCODE[@]}