chore(user/mw_get_audiobook_chapters.sh):Clean up desc
[BK-2020-03.git] / user / mp3s_to_mkv.sh
1 #!/bin/bash
2 # Desc: Converts a directory of mp3s into a single mkv file with chapters
3 # Usage: mp3s_to_mkv.sh [DIR in] [DIR out] [BITRATE]
4 # Example: mp3s_to_mkv.sh ./dir_source ./dir_output 48k
5 # Depends: GNU Coreutils 8.32 (date), ffmpeg, ffprobe
6 # Ref/Attrib: [1] FFmpeg Formats Documentation https://ffmpeg.org/ffmpeg-formats.html#toc-Metadata-2
7 # Version: 0.1.0
8
9 # plumbing
10 opus_bitrate="$3"; # e.g. "48k"
11 script_rundate="$(date +%s)";
12 dir_tmp="/dev/shm";
13 dir_in="$(readlink -f "$1")";
14 dir_out="$(readlink -f "$2")";
15 file_flist="$dir_tmp"/"$script_rundate"..flist.txt;
16 file_out_mkv="$dir_out"/output.mkv;
17 file_albumart="$dir_out"/albumart.png;
18 file_chapters="$dir_tmp"/"$script_rundate"..chapters.txt;
19
20 yell() { echo "$0: $*" >&2; } # print script path and all args to stderr
21 die() { yell "$*"; exit 111; } # same as yell() but non-zero exit status
22 must() { "$@" || die "cannot $*"; } # runs args as command, reports args if command fails
23 show_usage() {
24 # Desc: Display script usage information
25 # Usage: showUsage
26 # Version 0.0.2
27 # Input: none
28 # Output: stdout
29 # Depends: GNU-coreutils 8.30 (cat)
30 cat <<'EOF'
31 USAGE:
32 mp3s_to_mkv.sh [DIR in] [DIR out] [BITRATE]
33
34 EXAMPLE:
35 mp3s_to_mkv.sh ./source_dir ./out_dir 48k
36 EOF
37 } # Display information on how to use this script.
38 check_depends() {
39 if ! command -v ffmpeg; then show_usage; die "FATAL:Missing ffmpeg."; fi;
40 if ! command -v ffprobe; then show_usage; die "FATAL:Missing ffprobe."; fi;
41 }; # check dependencies
42 check_plumbing() {
43 if [[ $# -ne 3 ]]; then show_usage; die "FATAL:Invalid arg count:$#"; fi;
44 if [[ ! -d "$dir_in" ]]; then show_usage; die "FATAL:Not a dir:$dir_in"; fi;
45 if [[ ! -d "$dir_out" ]]; then mkdir -p "$2"; fi;
46 }; # check arguments
47 get_media_length() {
48 # Use ffprobe to get media container length in seconds (float)
49 # Usage: get_media_length arg1
50 # Input: arg1: path to file
51 # Output: stdout: seconds (float)
52 # Depends: ffprobe 4.1.8
53 # BK-2020-03: die()
54 local file_in
55 file_in="$1";
56 if [[ ! -f $file_in ]]; then
57 die "ERROR:Not a file:$file_in";
58 fi;
59 ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file_in";
60 } # Get media container length in seconds via stdout
61 build_filelist_and_chapters() {
62 # Depends: var dir_tmp temporary directory
63 # var dir_in input dir
64 # var file_flist path file list
65 # Output: file: $file_flist list of mp3 files for ffmpeg
66 # file: $file_chapters chapters file for ffmpeg
67
68 # Change directory to input dir
69 pushd "$dir_in" || die "FATAL:Directory error:$(pwd)";
70
71 local chapter_start=0;
72 local chapter_end;
73 local duration;
74
75
76 # Initialize chapter ffmetadata file. See [1].
77 {
78 echo ";FFMETADATA1";
79 } >> "$file_chapters";
80
81
82 find "$dir_in" -type f -iname "*.mp3" | sort | while read -r line; do
83 local filename="${line#./}";
84 yell "$(printf "file '%s'\n" "$filename")";
85 printf "file '%s'\n" "$filename" >> "$file_flist";
86
87 # Get duration of the current file
88 duration=$(get_media_length "$filename");
89 chapter_end=$(echo "$chapter_start + $duration" | bc -l);
90
91 # Write chapter info
92 {
93 echo "[CHAPTER]";
94 echo "TIMEBASE=1/1000";
95 echo "START=$(echo "scale=0; $chapter_start * 1000" | bc -l)";
96 echo "END=$(echo "scale=0; $chapter_end * 1000" | bc -l)";
97 echo "title=$(basename "$filename" .mp3)";
98 } >> "$file_chapters";
99
100 chapter_start=$chapter_end
101 done
102
103 # Return to original dir
104 popd || die "FATAL:Directory error:$(pwd)";
105 }; # build file list and chapters for ffmpeg
106 ffmpeg_convert() {
107 # Depends: var dir_tmp
108 # dir_in
109 # dir_out
110 # Input: file $file_flist list of mp3 files for ffmpeg
111 # file $file_chapters chapters file for ffmpeg
112
113 # Change directory to input dir
114 pushd "$dir_in" || die "FATAL:Directory error:$(pwd)";
115
116 # Concatenate mp3 files into a single WAV file
117 # Convert WAV to 48 kbps opus mkv file
118 ffmpeg -nostdin -f concat -safe 0 -i "$file_flist" -c:a pcm_s24le -rf64 auto -f wav - | \
119 ffmpeg -i - -i "$file_chapters" \
120 -map_metadata 1 -map_chapters 1 \
121 -map 0 -c:a libopus -b:a "$opus_bitrate" \
122 "$file_out_mkv";
123
124 # Return to original dir
125 popd || die "FATAL:Directory error:$(pwd)";
126 }; # convert mp3s to opus mkv via ffmpeg
127 save_albumart() {
128 local file
129 file="$(find "$dir_in" -type f -iname "*.mp3" | sort | head -n1)";
130 file="$(readlink -f "$file")";
131 ffmpeg -nostdin -i "$file" -an -vcodec copy "$file_albumart";
132 }; # save album art from an mp3 to output dir
133 main() {
134 check_depends && yell "DEBUG:check_depends OK";
135 check_plumbing "$@" && yell "DEBUG:check_plumbing OK";
136 build_filelist_and_chapters "$@" && yell "DEBUG:build_filelist_and_chapters OK";
137 ffmpeg_convert "$@" && yell "DEBUG:ffmpeg_convert OK";
138 save_albumart "$@" && yell "DEBUG:save_albumart OK";
139 }; # main program
140
141 main "$@";