7befc0b2a2c6fa4b1408ae7f771a456508016f7b
[BK-2020-03.git] / unitproc / bk_export_audio.sh
1 #!/bin/bash
2 # Desc: Extracts audio from video files
3 # Usage: bk_export_audio.sh [input_dir] ([output_dir])
4 # Version: 0.0.2
5
6 declare -Ag appRollCall # Associative array for storing app status
7 declare -Ag fileRollCall # Associative array for storing file status
8 declare -Ag dirRollCall # Associative array for storing dir status
9
10 yell() { echo "$0: $*" >&2; } # print script path and all args to stderr
11 die() { yell "$*"; exit 111; } # same as yell() but non-zero exit status
12 try() { "$@" || die "cannot $*"; } # runs args as command, reports args if command fails
13 checkapp() {
14 # Desc: If arg is a command, save result in assoc array 'appRollCall'
15 # Usage: checkapp arg1 arg2 arg3 ...
16 # Version: 0.1.1
17 # Input: global assoc. array 'appRollCall'
18 # Output: adds/updates key(value) to global assoc array 'appRollCall'
19 # Depends: bash 5.0.3
20 local returnState
21
22 #===Process Args===
23 for arg in "$@"; do
24 if command -v "$arg" 1>/dev/null 2>&1; then # Check if arg is a valid command
25 appRollCall[$arg]="true";
26 if ! [ "$returnState" = "false" ]; then returnState="true"; fi;
27 else
28 appRollCall[$arg]="false"; returnState="false";
29 fi;
30 done;
31
32 #===Determine function return code===
33 if [ "$returnState" = "true" ]; then
34 return 0;
35 else
36 return 1;
37 fi;
38 } # Check that app exists
39 checkfile() {
40 # Desc: If arg is a file path, save result in assoc array 'fileRollCall'
41 # Usage: checkfile arg1 arg2 arg3 ...
42 # Version: 0.1.1
43 # Input: global assoc. array 'fileRollCall'
44 # Output: adds/updates key(value) to global assoc array 'fileRollCall';
45 # Output: returns 0 if app found, 1 otherwise
46 # Depends: bash 5.0.3
47 local returnState
48
49 #===Process Args===
50 for arg in "$@"; do
51 if [ -f "$arg" ]; then
52 fileRollCall["$arg"]="true";
53 if ! [ "$returnState" = "false" ]; then returnState="true"; fi;
54 else
55 fileRollCall["$arg"]="false"; returnState="false";
56 fi;
57 done;
58
59 #===Determine function return code===
60 if [ "$returnState" = "true" ]; then
61 return 0;
62 else
63 return 1;
64 fi;
65 } # Check that file exists
66 checkdir() {
67 # Desc: If arg is a dir path, save result in assoc array 'dirRollCall'
68 # Usage: checkdir arg1 arg2 arg3 ...
69 # Version 0.1.2
70 # Input: global assoc. array 'dirRollCall'
71 # Output: adds/updates key(value) to global assoc array 'dirRollCall';
72 # Output: returns 0 if all args are dirs; 1 otherwise
73 # Depends: Bash 5.0.3
74 local returnState
75
76 #===Process Args===
77 for arg in "$@"; do
78 if [ -z "$arg" ]; then
79 dirRollCall["(Unspecified Dirname(s))"]="false"; returnState="false";
80 elif [ -d "$arg" ]; then
81 dirRollCall["$arg"]="true";
82 if ! [ "$returnState" = "false" ]; then returnState="true"; fi
83 else
84 dirRollCall["$arg"]="false"; returnState="false";
85 fi
86 done
87
88 #===Determine function return code===
89 if [ "$returnState" = "true" ]; then
90 return 0;
91 else
92 return 1;
93 fi
94 } # Check that dir exists
95 displayMissing() {
96 # Desc: Displays missing apps, files, and dirs
97 # Usage: displayMissing
98 # Version 0.1.1
99 # Input: associative arrays: appRollCall, fileRollCall, dirRollCall
100 # Output: stderr: messages indicating missing apps, file, or dirs
101 # Depends: bash 5, checkAppFileDir()
102 local missingApps value appMissing missingFiles fileMissing
103 local missingDirs dirMissing
104
105 #==BEGIN Display errors==
106 #===BEGIN Display Missing Apps===
107 missingApps="Missing apps :";
108 #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done
109 for key in "${!appRollCall[@]}"; do
110 value="${appRollCall[$key]}";
111 if [ "$value" = "false" ]; then
112 #echo "DEBUG:Missing apps: $key => $value";
113 missingApps="$missingApps""$key ";
114 appMissing="true";
115 fi;
116 done;
117 if [ "$appMissing" = "true" ]; then # Only indicate if an app is missing.
118 echo "$missingApps" 1>&2;
119 fi;
120 unset value;
121 #===END Display Missing Apps===
122
123 #===BEGIN Display Missing Files===
124 missingFiles="Missing files:";
125 #for key in "${!fileRollCall[@]}"; do echo "DEBUG:$key => ${fileRollCall[$key]}"; done
126 for key in "${!fileRollCall[@]}"; do
127 value="${fileRollCall[$key]}";
128 if [ "$value" = "false" ]; then
129 #echo "DEBUG:Missing files: $key => $value";
130 missingFiles="$missingFiles""$key ";
131 fileMissing="true";
132 fi;
133 done;
134 if [ "$fileMissing" = "true" ]; then # Only indicate if an app is missing.
135 echo "$missingFiles" 1>&2;
136 fi;
137 unset value;
138 #===END Display Missing Files===
139
140 #===BEGIN Display Missing Directories===
141 missingDirs="Missing dirs:";
142 #for key in "${!dirRollCall[@]}"; do echo "DEBUG:$key => ${dirRollCall[$key]}"; done
143 for key in "${!dirRollCall[@]}"; do
144 value="${dirRollCall[$key]}";
145 if [ "$value" = "false" ]; then
146 #echo "DEBUG:Missing dirs: $key => $value";
147 missingDirs="$missingDirs""$key ";
148 dirMissing="true";
149 fi;
150 done;
151 if [ "$dirMissing" = "true" ]; then # Only indicate if an dir is missing.
152 echo "$missingDirs" 1>&2;
153 fi;
154 unset value;
155 #===END Display Missing Directories===
156
157 #==END Display errors==
158 } # Display missing apps, files, dirs
159 showUsage() {
160 # Desc: Display script usage information
161 # Usage: showUsage
162 # Version 0.0.1
163 # Input: none
164 # Output: stdout
165 # Depends: GNU-coreutils 8.30 (cat)
166 cat <<'EOF'
167 USAGE:
168 bk_export_audio.sh [DIR in] ([DIR out])
169
170 EXAMPLE:
171 bk_export_audio.sh ./videos/ ./exported_audio/
172 bk_export_audio.sh ./videos/
173 EOF
174 } # Display information on how to use this script.
175 get_audio_format() {
176 # Desc: Gets audio format of file as string
177 # Usage: get_audio_format arg1
178 # Depends: ffprobe
179 # Version: 0.0.1
180 # Input: arg1: input file path
181 # Output: stdout (if valid audio format)
182 # exit code 0 if audio file; 1 otherwise
183 # Example: get_audio_format myvideo.mp4
184 # Note: Would return "opus" if full ffprobe report had 'Audio: opus, 48000 Hz, stereo, fltp'
185 # Note: Not tested with videos containing multiple video streams
186 # Ref/Attrib: [1] https://stackoverflow.com/questions/5618363/is-there-a-way-to-use-ffmpeg-to-determine-the-encoding-of-a-file-before-transcod
187 # [2] https://stackoverflow.com/questions/44123532/how-to-find-out-the-file-extension-for-extracting-audio-tracks-with-ffmpeg-and-p#comment88464070_50723126
188 local audio_format file_in;
189 local return_state;
190 file_in="$1";
191
192 # Return error exit code if not audio file
193 ## Return error if ffprobe itself exited on error
194 if ! ffprobe -v error -select_streams a -show_entries stream=codec_name -of default=nokey=1:noprint_wrappers=1 "$file_in" 1>/dev/null 2>&1; then
195 return_state="false";
196 fi;
197
198 # Get audio format
199 audio_format="$(ffprobe -v error -select_streams a -show_entries stream=codec_name -of default=nokey=1:noprint_wrappers=1 "$file_in")"; # see [1]
200
201 ## Return error if audio format is incorrectly formatted (e.g. reject if contains spaces)
202 pattern="^[[:alnum:]]+$"; # alphanumeric string with no spaces
203 if [[ $audio_format =~ $pattern ]]; then
204 return_state="true";
205 # Report audio format
206 echo "$audio_format";
207 else
208 return_state="false";
209 fi;
210
211 # Report exit code
212 if [[ $return_state = "true" ]]; then
213 return 0;
214 else
215 return 1;
216 fi;
217 } # Get audio format as stdout
218 extract_audio_file() {
219 # Desc: Use ffmpeg to creates audio file from input video file
220 # Usage: extract_audio_file arg1 arg2 arg3
221 # Depends: ffmpeg
222 # Input: arg1: input video file path
223 # arg2: desired output file extension
224 # arg3: output dir path
225 # Output: audio file at path [arg3]/[arg1].[arg2]
226 local file_in file_in_ext dir_out file_in_basename;
227 file_in="$1";
228 file_in_ext="$2";
229 dir_out="$3";
230
231 # Extract audio file
232 file_in_basename="$(basename "$file_in")";
233 ffmpeg -i "$file_in" -vn -acodec copy "$dir_out"/"$file_in_basename"."$file_in_ext";
234 } # Create audio file from video file
235
236 main() {
237 script_pwd="$(pwd)";
238 dir_in="$1";
239 dir_out="$2";
240
241 # Check argument count
242 if [[ $# -lt 1 ]]; then
243 showUsage;
244 die "ERROR:Not enough arguments:$#";
245 fi;
246
247 # Check apps, dirs
248 checkapp ffmpeg ffprobe date;
249 displayMissing;
250
251 if ! checkdir "$dir_in"; then
252 showUsage;
253 displayMissing;
254 die "ERROR:Missing input directory."
255 fi;
256 if ! checkdir "$dir_out"; then
257 yell "NOTICE:Output directory not specified. Creating output directory in current working directory:$script_pwd";
258 timestamp="$(date +%Y%m%dT%H%M%S%z)"; # iso-8601 without separators
259 dir_out="$script_pwd"/"$timestamp"..output;
260 try mkdir "$dir_out";
261 fi;
262
263 # Do work
264 yell "DEBUG:dir_in:$dir_in":
265 yell "DEBUG:dir_out:$dir_out";
266 for file in "$dir_in"/*; do
267 aud_format="$(get_audio_format "$file")"; # Get audio format as file extension string
268 file_basename="$(basename "$file")"; # Get basename for debugging
269 yell "DEBUG:file_basename:$file_basename";
270 yell "DEBUG:aud_format:$aud_format";
271 yell "DEBUG:";
272
273 # Ignore files that return blank $aud_format
274 if [[ -z $aud_format ]]; then
275 yell "DEBUG:Not an audio file:$file";
276 continue;
277 fi;
278
279 extract_audio_file "$file" "$aud_format" "$dir_out";
280 done;
281 } # main program
282
283 main "$@";
284
285 # Author: Steven Baltaktei Sandoval
286 # License: GPLv3+