chore(user/mp3s_to_mkv.sh):Add license
[BK-2020-03.git] / user / mp3s_to_mkv.sh
CommitLineData
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
26429c01 7# Version: 0.1.1
a4b60a6d
SBS
8
9# plumbing
10opus_bitrate="$3"; # e.g. "48k"
11script_rundate="$(date +%s)";
12dir_tmp="/dev/shm";
13dir_in="$(readlink -f "$1")";
14dir_out="$(readlink -f "$2")";
15file_flist="$dir_tmp"/"$script_rundate"..flist.txt;
16file_out_mkv="$dir_out"/output.mkv;
17file_albumart="$dir_out"/albumart.png;
9a4c8b88 18file_chapters="$dir_tmp"/"$script_rundate"..chapters.txt;
a4b60a6d
SBS
19
20yell() { echo "$0: $*" >&2; } # print script path and all args to stderr
21die() { yell "$*"; exit 111; } # same as yell() but non-zero exit status
22must() { "$@" || die "cannot $*"; } # runs args as command, reports args if command fails
23show_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
36EOF
37} # Display information on how to use this script.
38check_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
42check_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
47get_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
61build_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
106ffmpeg_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
127save_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
133main() {
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
141main "$@";
26429c01
SBS
142
143# Author: Steven Baltakatei Sandoval
144# License: GPLv3+
145