diff options
Diffstat (limited to '')
| -rwxr-xr-x | transcode.sh | 203 |
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[@]} |
