Commit | Line | Data |
---|---|---|
a4b60a6d | 1 | #!/bin/bash |
9a4c8b88 | 2 | # Desc: Converts a directory of mp3s into a single mkv file with chapters |
a4b60a6d SBS |
3 | # Usage: mp3s_to_mkv.sh [DIR in] [DIR out] [BITRATE] |
4 | # Example: mp3s_to_mkv.sh ./dir_source ./dir_output 48k | |
9a4c8b88 SBS |
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 | |
a4b60a6d SBS |
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; | |
9a4c8b88 | 18 | file_chapters="$dir_tmp"/"$script_rundate"..chapters.txt; |
a4b60a6d SBS |
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; | |
9a4c8b88 | 40 | if ! command -v ffprobe; then show_usage; die "FATAL:Missing ffprobe."; fi; |
a4b60a6d SBS |
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 | |
9a4c8b88 SBS |
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() { | |
a4b60a6d SBS |
62 | # Depends: var dir_tmp temporary directory |
63 | # var dir_in input dir | |
64 | # var file_flist path file list | |
9a4c8b88 SBS |
65 | # Output: file: $file_flist list of mp3 files for ffmpeg |
66 | # file: $file_chapters chapters file for ffmpeg | |
a4b60a6d SBS |
67 | |
68 | # Change directory to input dir | |
69 | pushd "$dir_in" || die "FATAL:Directory error:$(pwd)"; | |
9a4c8b88 SBS |
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 | ||
a4b60a6d | 81 | |
9a4c8b88 SBS |
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 | |
a4b60a6d SBS |
102 | |
103 | # Return to original dir | |
104 | popd || die "FATAL:Directory error:$(pwd)"; | |
9a4c8b88 | 105 | }; # build file list and chapters for ffmpeg |
a4b60a6d SBS |
106 | ffmpeg_convert() { |
107 | # Depends: var dir_tmp | |
108 | # dir_in | |
109 | # dir_out | |
9a4c8b88 SBS |
110 | # Input: file $file_flist list of mp3 files for ffmpeg |
111 | # file $file_chapters chapters file for ffmpeg | |
a4b60a6d SBS |
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 | |
9a4c8b88 | 117 | # Convert WAV to 48 kbps opus mkv file |
42560010 | 118 | ffmpeg -nostdin -f concat -safe 0 -i "$file_flist" -c:a pcm_s24le -rf64 auto -f wav - | \ |
9a4c8b88 SBS |
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"; | |
a4b60a6d SBS |
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")"; | |
42560010 | 131 | ffmpeg -nostdin -i "$file" -an -vcodec copy "$file_albumart"; |
a4b60a6d SBS |
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"; | |
9a4c8b88 | 136 | build_filelist_and_chapters "$@" && yell "DEBUG:build_filelist_and_chapters OK"; |
a4b60a6d SBS |
137 | ffmpeg_convert "$@" && yell "DEBUG:ffmpeg_convert OK"; |
138 | save_albumart "$@" && yell "DEBUG:save_albumart OK"; | |
139 | }; # main program | |
140 | ||
141 | main "$@"; |