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
10 opus_bitrate
="$3"; # e.g. "48k"
11 script_rundate
="$(date +%s)";
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
;
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
24 # Desc: Display script usage information
29 # Depends: GNU-coreutils 8.30 (cat)
32 mp3s_to_mkv.sh [DIR in] [DIR out] [BITRATE]
35 mp3s_to_mkv.sh ./source_dir ./out_dir 48k
37 } # Display information on how to use this script.
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
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;
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
56 if [[ ! -f $file_in ]]; then
57 die
"ERROR:Not a file:$file_in";
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
68 # Change directory to input dir
69 pushd "$dir_in" || die
"FATAL:Directory error:$(pwd)";
71 local chapter_start
=0;
76 # Initialize chapter ffmetadata file. See [1].
79 } >> "$file_chapters";
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";
87 # Get duration of the current file
88 duration
=$
(get_media_length
"$filename");
89 chapter_end
=$
(echo "$chapter_start + $duration" |
bc -l);
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";
100 chapter_start
=$chapter_end
103 # Return to original dir
104 popd || die
"FATAL:Directory error:$(pwd)";
105 }; # build file list and chapters for ffmpeg
107 # Depends: var dir_tmp
110 # Input: file $file_flist list of mp3 files for ffmpeg
111 # file $file_chapters chapters file for ffmpeg
113 # Change directory to input dir
114 pushd "$dir_in" || die
"FATAL:Directory error:$(pwd)";
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" \
124 # Return to original dir
125 popd || die
"FATAL:Directory error:$(pwd)";
126 }; # convert mp3s to opus mkv via ffmpeg
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
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";