X-Git-Url: https://zdv2.bktei.com/gitweb/BK-2020-03.git/blobdiff_plain/65207f302c1787df989174cebd4e383ef72eda19..c21cf1161003f00b20da69cd9054220cea9268eb:/user/mp3s_to_mkv.sh diff --git a/user/mp3s_to_mkv.sh b/user/mp3s_to_mkv.sh index eb32982..b7fd29e 100755 --- a/user/mp3s_to_mkv.sh +++ b/user/mp3s_to_mkv.sh @@ -1,9 +1,10 @@ #!/bin/bash -# Desc: Converts a directory of mp3s into a single mkv file +# Desc: Converts a directory of mp3s into a single mkv file with chapters # Usage: mp3s_to_mkv.sh [DIR in] [DIR out] [BITRATE] # Example: mp3s_to_mkv.sh ./dir_source ./dir_output 48k -# Depends: GNU Coretils 8.32 (date) -# Version: 0.0.1 +# Depends: GNU Coreutils 8.32 (date), ffmpeg, ffprobe +# Ref/Attrib: [1] FFmpeg Formats Documentation https://ffmpeg.org/ffmpeg-formats.html#toc-Metadata-2 +# Version: 0.1.1 # plumbing opus_bitrate="$3"; # e.g. "48k" @@ -14,6 +15,7 @@ dir_out="$(readlink -f "$2")"; file_flist="$dir_tmp"/"$script_rundate"..flist.txt; file_out_mkv="$dir_out"/output.mkv; file_albumart="$dir_out"/albumart.png; +file_chapters="$dir_tmp"/"$script_rundate"..chapters.txt; yell() { echo "$0: $*" >&2; } # print script path and all args to stderr die() { yell "$*"; exit 111; } # same as yell() but non-zero exit status @@ -35,43 +37,89 @@ EOF } # Display information on how to use this script. check_depends() { if ! command -v ffmpeg; then show_usage; die "FATAL:Missing ffmpeg."; fi; + if ! command -v ffprobe; then show_usage; die "FATAL:Missing ffprobe."; fi; }; # check dependencies check_plumbing() { if [[ $# -ne 3 ]]; then show_usage; die "FATAL:Invalid arg count:$#"; fi; if [[ ! -d "$dir_in" ]]; then show_usage; die "FATAL:Not a dir:$dir_in"; fi; if [[ ! -d "$dir_out" ]]; then mkdir -p "$2"; fi; }; # check arguments -build_filelist() { +get_media_length() { + # Use ffprobe to get media container length in seconds (float) + # Usage: get_media_length arg1 + # Input: arg1: path to file + # Output: stdout: seconds (float) + # Depends: ffprobe 4.1.8 + # BK-2020-03: die() + local file_in; + file_in="$1"; + if [[ ! -f $file_in ]]; then + die "ERROR:Not a file:$file_in"; + fi; + ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file_in"; +} # Get media container length in seconds via stdout +build_filelist_and_chapters() { # Depends: var dir_tmp temporary directory # var dir_in input dir # var file_flist path file list - # Output: file: $file_flist list of mp3 files for ffmpeg + # Output: file: $file_flist list of mp3 files for ffmpeg + # file: $file_chapters chapters file for ffmpeg # Change directory to input dir pushd "$dir_in" || die "FATAL:Directory error:$(pwd)"; + + local chapter_start=0; + local chapter_end; + local duration; + + + # Initialize chapter ffmetadata file. See [1]. + { + echo ";FFMETADATA1"; + } >> "$file_chapters"; + - while read -r line; do - yell "$(printf "file '%s'\n" "${line#./}")"; - printf "file '%s'\n" "${line#./}" >> "$file_flist"; - done < <(find "$dir_in" -type f -iname "*.mp3" | sort); + find "$dir_in" -type f -iname "*.mp3" | sort | while read -r line; do + local filename="${line#./}"; + yell "$(printf "file '%s'\n" "$filename")"; + printf "file '%s'\n" "$filename" >> "$file_flist"; + + # Get duration of the current file + duration=$(get_media_length "$filename"); + chapter_end=$(echo "$chapter_start + $duration" | bc -l); + + # Write chapter info + { + echo "[CHAPTER]"; + echo "TIMEBASE=1/1000"; + echo "START=$(echo "scale=0; $chapter_start * 1000" | bc -l)"; + echo "END=$(echo "scale=0; $chapter_end * 1000" | bc -l)"; + echo "title=$(basename "$filename" .mp3)"; + } >> "$file_chapters"; + + chapter_start=$chapter_end; + done # Return to original dir popd || die "FATAL:Directory error:$(pwd)"; - -}; # build file list for ffmpeg +}; # build file list and chapters for ffmpeg ffmpeg_convert() { # Depends: var dir_tmp # dir_in # dir_out - # Input: file $file_flist list of mp3 files for ffmpeg + # Input: file $file_flist list of mp3 files for ffmpeg + # file $file_chapters chapters file for ffmpeg # Change directory to input dir pushd "$dir_in" || die "FATAL:Directory error:$(pwd)"; # Concatenate mp3 files into a single WAV file - # # Convert WAV to 48 kbps opus mkv file - ffmpeg -f concat -safe 0 -i "$file_flist" -c:a pcm_s24le -rf64 auto -f wav - | \ - ffmpeg -i - -c:a libopus -b:a "$opus_bitrate" "$file_out_mkv"; + # Convert WAV to 48 kbps opus mkv file + ffmpeg -nostdin -f concat -safe 0 -i "$file_flist" -c:a pcm_s24le -rf64 auto -f wav - | \ + ffmpeg -i - -i "$file_chapters" \ + -map_metadata 1 -map_chapters 1 \ + -map 0 -c:a libopus -b:a "$opus_bitrate" \ + "$file_out_mkv"; # Return to original dir popd || die "FATAL:Directory error:$(pwd)"; @@ -80,15 +128,17 @@ save_albumart() { local file file="$(find "$dir_in" -type f -iname "*.mp3" | sort | head -n1)"; file="$(readlink -f "$file")"; - ffmpeg -i "$file" -an -vcodec copy "$file_albumart"; + ffmpeg -nostdin -i "$file" -an -vcodec copy "$file_albumart"; }; # save album art from an mp3 to output dir main() { check_depends && yell "DEBUG:check_depends OK"; check_plumbing "$@" && yell "DEBUG:check_plumbing OK"; - build_filelist "$@" && yell "DEBUG:build_filelist OK"; + build_filelist_and_chapters "$@" && yell "DEBUG:build_filelist_and_chapters OK"; ffmpeg_convert "$@" && yell "DEBUG:ffmpeg_convert OK"; save_albumart "$@" && yell "DEBUG:save_albumart OK"; }; # main program main "$@"; +# Author: Steven Baltakatei Sandoval +# License: GPLv3+