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