concat.sh

Browser view only. Use the download link if you want to save the file.

Back to Toolbox
#!/bin/bash
SKIP_PEAK_ANALYSIS=1

source ./launch_terminal.sh
source ./load_video_files.sh
source ./pick_conversions.sh

# 0 = fastest       (copy video, copy audio)        [default]
# 1 = quick spackle (copy video, re-encode audio)
# 2 = full safe     (re-encode video to x265, audio to FLAC, output MKV)
SAFE_MODE=0


my-notify () {
    local message="$1"
    USE_SYSTEM_NOTIFY="0"
    statusfile="$HOME/status.log"
    this_script=$(basename "$script_name" .sh)

    if [ "$USE_SYSTEM_NOTIFY" == "1" ]; then
        notify-send -t 3000 "$message"
    else
        echo -e "${color} $this_script: \033[0m $message" >> "$statusfile"
    fi
}


show_mode_banner () {
    echo
    case "$1" in
        0)
            echo -e "\e[1;97;46m [ MODE 0 ] FAST CONCAT — copy video, copy audio \e[0m"
            ;;
        1)
            echo -e "\e[1;30;43m [ MODE 1 ] QUICK SPACKLE — copy video, re-encode audio \e[0m"
            ;;
        2)
            echo -e "\e[1;97;45m [ MODE 2 ] FULL SAFE + ARCHIVE — x265 + FLAC -> MKV \e[0m"
            ;;
    esac
    echo
}


show_detect_banner () {
    echo
    echo -e "\e[1;97;44m [ DETECT ] Consistent Ninja / Gen1 batch filenames detected \e[0m"
    echo -e "\e[1;97;44m [ DETECT ] Mode 0 is likely safe and faster for this batch \e[0m"
    echo
}


show_fail_banner () {
    echo
    echo -e "\e[1;97;41m [ FAIL ] Concatenation failed \e[0m"
    echo
}


show_suggest_banner () {
    local suggestion="$1"
    echo -e "\e[1;30;43m [ SUGGEST ] $suggestion \e[0m"
}


detect_consistent_batch () {
    local f
    local base
    local matched_any=0

    for f in "${filenames[@]}"; do
        base=$(basename "$f")

        if [[ "$base" =~ ^NinjaV.*\.(mov|MOV|mkv|MKV)$ ]] || \
           [[ "$base" =~ ^Njaflm.*\.(mov|MOV|mkv|MKV)$ ]] || \
           [[ "$base" =~ ^SgnFlm.*\.(mov|MOV|mkv|MKV)$ ]]; then
            matched_any=1
        else
            return 1
        fi
    done

    [ "$matched_any" -eq 1 ]
}


suggest_retry_mode () {
    local logfile="$1"

    # If Mode 1 already failed, jump to Mode 2 suggestion.
    if [ "$SAFE_MODE" = "1" ]; then
        show_suggest_banner "Retry with SAFE_MODE=2 for full safe archive MKV mode."
        return
    fi

    # If Mode 2 already failed, no higher mode remains.
    if [ "$SAFE_MODE" = "2" ]; then
        show_suggest_banner "SAFE_MODE=2 already used. Problem is likely source-file related."
        return
    fi

    # Mode 0 failed: try to distinguish likely audio/timestamp trouble from broader mismatch trouble.
    if grep -Eqi \
        'non[- ]?monoton|dts|invalid.*dts|aac|audio|Queue input is backward in time|malformed aac|timestamp' \
        "$logfile"; then
        show_suggest_banner "Retry with SAFE_MODE=1 for quick audio/timestamp repair."
    else
        show_suggest_banner "Retry with SAFE_MODE=2 for full safe archive MKV mode."
    fi
}


run_ffmpeg_live () {
    local logfile="$1"

    if [ "$SAFE_MODE" = "2" ]; then
        ffmpeg -fflags +genpts -avoid_negative_ts make_zero \
            -f concat -safe 0 -i "mylist.txt" \
            -c:v libx265 -crf 20 -preset medium \
            -c:a flac \
            ${FFMPEG_EXTRA_ARGS[@]} \
            "$output_file" 2>&1 | tee "$logfile"
        return ${PIPESTATUS[0]}

    elif [ "$SAFE_MODE" = "1" ]; then
        ffmpeg -fflags +genpts -avoid_negative_ts make_zero \
            -f concat -safe 0 -i "mylist.txt" \
            -c:v copy -c:a aac -b:a 256k \
            ${FFMPEG_EXTRA_ARGS[@]} \
            "$output_file" 2>&1 | tee "$logfile"
        return ${PIPESTATUS[0]}

    else
        ffmpeg -fflags +genpts -avoid_negative_ts make_zero \
            -f concat -safe 0 -i "mylist.txt" \
            -c:v copy -c:a copy \
            ${FFMPEG_EXTRA_ARGS[@]} \
            "$output_file" 2>&1 | tee "$logfile"
        return ${PIPESTATUS[0]}
    fi
}


# Get the extension from the first enabled video file
ext=""
for i in "${!filenames[@]}"; do
    ext="${filenames[$i]##*.}"
    break
done

# If no enabled files were found, exit the script
if [ -z "$ext" ]; then
    echo "No enabled video files to concatenate. Exiting in 5 secs..."
    sleep 5
    kill -- "$PPID"
    exit 1
fi

# If detection finds a likely consistent Ninja / Gen1 batch, offer Mode 0
if detect_consistent_batch; then
    show_detect_banner
    read -rsn1 -p $'\e[1;97;44m Switch to MODE 0 for this detected batch? (Y/N) \e[0m ' mode0_choice
    echo

    if [[ "$mode0_choice" =~ [Yy] ]]; then
        SAFE_MODE=0
        echo -e "\e[92mUsing detected fast path: SAFE_MODE=0\e[0m"
    else
        echo -e "\e[93mKeeping current SAFE_MODE=$SAFE_MODE\e[0m"
    fi
    echo
fi

# --- FFDASH PROGRESS SUPPORT ---
FFDASH_DIR="/tmp/ffdash"
mkdir -p "$FFDASH_DIR"

if [ "$SAFE_MODE" = "2" ]; then
    output_file="joined.mkv"
else
    output_file="joined.$ext"
fi

jobbase="${output_file##*/}"
paramfile="$FFDASH_DIR/${jobbase}.params"
progressfile="$FFDASH_DIR/${jobbase}.progress"
fflog="/tmp/concat_ffmpeg_${jobbase}_$$.log"

# Build the concat list
echo "Generating file list for concatenation..."
> mylist.txt
basename_guess=""
for i in "${!filenames[@]}"; do
    echo "file '$(basename "${filenames[$i]}")'" >> mylist.txt
    if [[ -z "$basename_guess" ]]; then
        if [[ "${filenames[$i]}" =~ ^(.*)[[:space:]]+[0-9]+\..*$ ]]; then
            basename_guess="${BASH_REMATCH[1]}"
        fi
    fi
done

# Display chosen mode
show_mode_banner "$SAFE_MODE"

# Display the list and guess
echo -e "Best guess at good filename : \e[1;97;44m ${basename_guess##*/} \e[0m"
echo "The following files will be concatenated:"
cat mylist.txt
sleep 2

ffmpeg_status=1
FFMPEG_EXTRA_ARGS=()

# If ffdash is active, create progress tracking files
if [[ -f "$HOME/bin/ffdash_running.flag" ]]; then
    echo "Detected ffdash is running — enabling live progress tracking."

    total_duration=0
    for f in "${filenames[@]}"; do
        dur=$(ffprobe -v error -show_entries format=duration \
              -of default=noprint_wrappers=1:nokey=1 "$f" 2>/dev/null)
        total_duration=$(awk "BEGIN {print $total_duration + $dur}")
    done

    cat >"$paramfile" <<EOF
src=mylist.txt
out=$output_file
duration=$total_duration
job=1
total=1
EOF

    FFMPEG_EXTRA_ARGS=(-progress "$progressfile")
    run_ffmpeg_live "$fflog"
    ffmpeg_status=$?
    rm -f "$progressfile" "$paramfile"

else
    echo "ffdash not active — running normally."

    FFMPEG_EXTRA_ARGS=()
    run_ffmpeg_live "$fflog"
    ffmpeg_status=$?
fi

# Clean up
rm -f mylist.txt
# --- END FFDASH PROGRESS SUPPORT ---

# Display completion message
if [ "$ffmpeg_status" -eq 0 ]; then
    my-notify "Merge successful -> $output_file"
    echo -e "\e[92mConcatenation completed successfully. Output: $output_file\e[0m"

    # Convert extension to uppercase
    ext_upper=$(echo "${output_file##*.}" | tr '[:lower:]' '[:upper:]')

    # Prompt user to move original files
    read -rsn1 -p $'\e[1;97;44m Do you want to move the original files to '\''orig '"$ext_upper"' files'\''? (Y/N) ' move_choice
    echo -e "\e[0m"

    if [[ "$move_choice" =~ [Yy] ]]; then
        dest_folder="orig $ext_upper files"
        mkdir -p "$dest_folder"

        for i in "${!filenames[@]}"; do
            mv "${filenames[$i]}" "$dest_folder"
        done

        echo -e "\n\e[92mFiles moved to '$dest_folder'\e[0m"
    else
        echo -e "\n\e[93mFiles were not moved.\e[0m"
    fi

    # Offer to rename the output file to the guessed basename if it's found
    if [[ -n "$basename_guess" ]]; then
        if [ "$SAFE_MODE" = "2" ]; then
            new_output_file="$basename_guess.mkv"
        else
            new_output_file="$basename_guess.$ext"
        fi

        read -rsn1 -p $'\e[1;97;44m Do you want to rename the output file to '"${new_output_file##*/}"'? (Y/N) ' rename_choice
        echo -e "\e[0m"

        if [[ "$rename_choice" =~ [Yy] ]]; then
            mv "$output_file" "$new_output_file"
            my-notify "${output_file##*/} -> ${new_output_file##*/}"
            output_file="$new_output_file"
            echo -e "\n\e[92mFile renamed to '$new_output_file'\e[0m"
        else
            echo -e "\n\e[93mFile was not renamed.\e[0m"
        fi
    fi

else
    my-notify "Concat failed!"
    show_fail_banner
    suggest_retry_mode "$fflog"
    echo -e "\e[90mLog saved to: $fflog\e[0m"
fi

# Prompt the user to press any key to exit
echo "Press any key to exit..."
read -n1
kill -- "$PPID"
exit 0