Commit | Line | Data |
---|---|---|
a9a36cc4 | 1 | #!/usr/bin/env bash |
40fcffff | 2 | # Desc: Copies random audio files |
a9a36cc4 | 3 | # Usage: bk-copy-rand-music.sh [dir SOURCE] [dir DEST] [int DURATION] |
40fcffff | 4 | # Version: 0.0.2 |
a9a36cc4 SBS |
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 | declare -a music_codecs # Array for storing valid codec names (e.g. "aac" "mp3") | |
10 | ||
11 | # Adjustable parameters | |
12 | music_codecs=("vorbis" "aac" "mp3" "flac" "opus"); # whitelist of valid codec_names ffprobe might return | |
40fcffff SBS |
13 | max_loops="1000000"; # max number of files to test whether are audio or not |
14 | max_filename_length="255"; # max output filename length | |
15 | min_file_duration="10"; # minimum duration per music file | |
a9a36cc4 SBS |
16 | |
17 | yell() { echo "$0: $*" >&2; } # print script path and all args to stderr | |
18 | die() { yell "$*"; exit 111; } # same as yell() but non-zero exit status | |
19 | try() { "$@" || die "cannot $*"; } # runs args as command, reports args if command fails | |
20 | checkapp() { | |
21 | # Desc: If arg is a command, save result in assoc array 'appRollCall' | |
22 | # Usage: checkapp arg1 arg2 arg3 ... | |
23 | # Version: 0.1.1 | |
24 | # Input: global assoc. array 'appRollCall' | |
25 | # Output: adds/updates key(value) to global assoc array 'appRollCall' | |
26 | # Depends: bash 5.0.3 | |
27 | local returnState | |
28 | ||
29 | #===Process Args=== | |
30 | for arg in "$@"; do | |
31 | if command -v "$arg" 1>/dev/null 2>&1; then # Check if arg is a valid command | |
32 | appRollCall[$arg]="true"; | |
33 | if ! [ "$returnState" = "false" ]; then returnState="true"; fi; | |
34 | else | |
35 | appRollCall[$arg]="false"; returnState="false"; | |
36 | fi; | |
37 | done; | |
38 | ||
39 | #===Determine function return code=== | |
40 | if [ "$returnState" = "true" ]; then | |
41 | return 0; | |
42 | else | |
43 | return 1; | |
44 | fi; | |
45 | } # Check that app exists | |
46 | checkfile() { | |
47 | # Desc: If arg is a file path, save result in assoc array 'fileRollCall' | |
48 | # Usage: checkfile arg1 arg2 arg3 ... | |
49 | # Version: 0.1.1 | |
50 | # Input: global assoc. array 'fileRollCall' | |
51 | # Output: adds/updates key(value) to global assoc array 'fileRollCall'; | |
52 | # Output: returns 0 if app found, 1 otherwise | |
53 | # Depends: bash 5.0.3 | |
54 | local returnState | |
55 | ||
56 | #===Process Args=== | |
57 | for arg in "$@"; do | |
58 | if [ -f "$arg" ]; then | |
59 | fileRollCall["$arg"]="true"; | |
60 | if ! [ "$returnState" = "false" ]; then returnState="true"; fi; | |
61 | else | |
62 | fileRollCall["$arg"]="false"; returnState="false"; | |
63 | fi; | |
64 | done; | |
65 | ||
66 | #===Determine function return code=== | |
67 | if [ "$returnState" = "true" ]; then | |
68 | return 0; | |
69 | else | |
70 | return 1; | |
71 | fi; | |
72 | } # Check that file exists | |
73 | checkdir() { | |
74 | # Desc: If arg is a dir path, save result in assoc array 'dirRollCall' | |
75 | # Usage: checkdir arg1 arg2 arg3 ... | |
76 | # Version 0.1.2 | |
77 | # Input: global assoc. array 'dirRollCall' | |
78 | # Output: adds/updates key(value) to global assoc array 'dirRollCall'; | |
79 | # Output: returns 0 if all args are dirs; 1 otherwise | |
80 | # Depends: Bash 5.0.3 | |
81 | local returnState | |
82 | ||
83 | #===Process Args=== | |
84 | for arg in "$@"; do | |
85 | if [ -z "$arg" ]; then | |
86 | dirRollCall["(Unspecified Dirname(s))"]="false"; returnState="false"; | |
87 | elif [ -d "$arg" ]; then | |
88 | dirRollCall["$arg"]="true"; | |
89 | if ! [ "$returnState" = "false" ]; then returnState="true"; fi | |
90 | else | |
91 | dirRollCall["$arg"]="false"; returnState="false"; | |
92 | fi | |
93 | done | |
94 | ||
95 | #===Determine function return code=== | |
96 | if [ "$returnState" = "true" ]; then | |
97 | return 0; | |
98 | else | |
99 | return 1; | |
100 | fi | |
101 | } # Check that dir exists | |
102 | displayMissing() { | |
103 | # Desc: Displays missing apps, files, and dirs | |
104 | # Usage: displayMissing | |
105 | # Version 1.0.0 | |
106 | # Input: associative arrays: appRollCall, fileRollCall, dirRollCall | |
107 | # Output: stderr: messages indicating missing apps, file, or dirs | |
108 | # Output: returns exit code 0 if nothing missing; 1 otherwise | |
109 | # Depends: bash 5, checkAppFileDir() | |
110 | local missingApps value appMissing missingFiles fileMissing | |
111 | local missingDirs dirMissing | |
112 | ||
113 | #==BEGIN Display errors== | |
114 | #===BEGIN Display Missing Apps=== | |
115 | missingApps="Missing apps :"; | |
116 | #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done | |
117 | for key in "${!appRollCall[@]}"; do | |
118 | value="${appRollCall[$key]}"; | |
119 | if [ "$value" = "false" ]; then | |
120 | #echo "DEBUG:Missing apps: $key => $value"; | |
121 | missingApps="$missingApps""$key "; | |
122 | appMissing="true"; | |
123 | fi; | |
124 | done; | |
125 | if [ "$appMissing" = "true" ]; then # Only indicate if an app is missing. | |
126 | echo "$missingApps" 1>&2; | |
127 | fi; | |
128 | unset value; | |
129 | #===END Display Missing Apps=== | |
130 | ||
131 | #===BEGIN Display Missing Files=== | |
132 | missingFiles="Missing files:"; | |
133 | #for key in "${!fileRollCall[@]}"; do echo "DEBUG:$key => ${fileRollCall[$key]}"; done | |
134 | for key in "${!fileRollCall[@]}"; do | |
135 | value="${fileRollCall[$key]}"; | |
136 | if [ "$value" = "false" ]; then | |
137 | #echo "DEBUG:Missing files: $key => $value"; | |
138 | missingFiles="$missingFiles""$key "; | |
139 | fileMissing="true"; | |
140 | fi; | |
141 | done; | |
142 | if [ "$fileMissing" = "true" ]; then # Only indicate if an app is missing. | |
143 | echo "$missingFiles" 1>&2; | |
144 | fi; | |
145 | unset value; | |
146 | #===END Display Missing Files=== | |
147 | ||
148 | #===BEGIN Display Missing Directories=== | |
149 | missingDirs="Missing dirs:"; | |
150 | #for key in "${!dirRollCall[@]}"; do echo "DEBUG:$key => ${dirRollCall[$key]}"; done | |
151 | for key in "${!dirRollCall[@]}"; do | |
152 | value="${dirRollCall[$key]}"; | |
153 | if [ "$value" = "false" ]; then | |
154 | #echo "DEBUG:Missing dirs: $key => $value"; | |
155 | missingDirs="$missingDirs""$key "; | |
156 | dirMissing="true"; | |
157 | fi; | |
158 | done; | |
159 | if [ "$dirMissing" = "true" ]; then # Only indicate if an dir is missing. | |
160 | echo "$missingDirs" 1>&2; | |
161 | fi; | |
162 | unset value; | |
163 | #===END Display Missing Directories=== | |
164 | ||
165 | #==END Display errors== | |
166 | #==BEGIN Determine function return code=== | |
167 | if [ "$appMissing" == "true" ] || [ "$fileMissing" == "true" ] || [ "$dirMissing" == "true" ]; then | |
168 | return 1; | |
169 | else | |
170 | return 0; | |
171 | fi | |
172 | #==END Determine function return code=== | |
173 | } # Display missing apps, files, dirs | |
174 | showUsage() { | |
175 | # Desc: Display script usage information | |
176 | # Usage: showUsage | |
177 | # Version 0.0.1 | |
178 | # Input: none | |
179 | # Output: stdout | |
180 | # Depends: GNU-coreutils 8.30 (cat) | |
181 | cat <<'EOF' | |
182 | ||
183 | DESCRIPTION: | |
40fcffff SBS |
184 | This script may be used to copy a random selection of files containing |
185 | audio tracks from SOURCE to DEST. | |
a9a36cc4 SBS |
186 | |
187 | USAGE: | |
188 | bk-copy-rand-music [dir SOURCE] [dir DEST] [int DURATION] | |
189 | ||
190 | EXAMPLE: | |
191 | bk-copy-rand-music ~/Music /tmp/music-sample 3600 | |
192 | ||
193 | DEPENDENCIES: | |
194 | ffprobe | |
195 | GNU Coreutils 8.30 | |
196 | EOF | |
197 | } # Display information on how to use this script. | |
198 | check_parsable_audio_ffprobe() { | |
199 | # Desc: Checks if ffprobe returns valid audio codec name for file | |
200 | # Usage: check_parsable_audio_ffprobe [path FILE] | |
201 | # Version: 0.0.1 | |
202 | # Input: arg1: file path | |
203 | # Output: exit code 0 if returns valid codec name; 1 otherwise | |
204 | # Depends: ffprobe, die() | |
205 | local file_in ffprobe_out | |
206 | ||
207 | if [[ $# -ne 1 ]]; then die "ERROR:Invalid number of args:$#"; fi; | |
208 | ||
209 | file_in="$1"; | |
210 | ||
211 | # Check if ffprobe detects an audio stream | |
212 | 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 | |
213 | return_state="true"; | |
214 | else | |
215 | return_state="false"; | |
216 | fi; | |
217 | ||
218 | # Fail if ffprobe returns no result | |
219 | ffprobe_out="$(ffprobe -v error -select_streams a -show_entries stream=codec_name -of default=nokey=1:noprint_wrappers=1 "$file_in")"; | |
220 | if [[ -z $ffprobe_out ]]; then | |
221 | return_state="false"; | |
222 | fi; | |
223 | ||
224 | # Report exit code | |
225 | if [[ $return_state = "true" ]]; then | |
226 | return 0; | |
227 | else | |
228 | return 1; | |
229 | fi; | |
230 | } # Checks if file has valid codec name using ffprobe | |
231 | get_audio_format() { | |
232 | # Desc: Gets audio format of file as string | |
233 | # Usage: get_audio_format arg1 | |
234 | # Depends: ffprobe | |
235 | # Version: 0.0.1 | |
236 | # Input: arg1: input file path | |
237 | # Output: stdout (if valid audio format) | |
238 | # exit code 0 if audio file; 1 otherwise | |
239 | # Example: get_audio_format myvideo.mp4 | |
240 | # Note: Would return "opus" if full ffprobe report had 'Audio: opus, 48000 Hz, stereo, fltp' | |
241 | # Note: Not tested with videos containing multiple video streams | |
242 | # Ref/Attrib: [1] https://stackoverflow.com/questions/5618363/is-there-a-way-to-use-ffmpeg-to-determine-the-encoding-of-a-file-before-transcod | |
243 | # [2] https://stackoverflow.com/questions/44123532/how-to-find-out-the-file-extension-for-extracting-audio-tracks-with-ffmpeg-and-p#comment88464070_50723126 | |
244 | local audio_format file_in; | |
245 | local return_state; | |
246 | file_in="$1"; | |
247 | ||
248 | # Return error exit code if not audio file | |
249 | ## Return error if ffprobe itself exited on error | |
250 | 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 | |
251 | return_state="false"; | |
252 | fi; | |
253 | ||
254 | # Get audio format | |
255 | audio_format="$(ffprobe -v error -select_streams a -show_entries stream=codec_name -of default=nokey=1:noprint_wrappers=1 "$file_in")"; # see [1] | |
256 | ||
257 | ## Return error if audio format is incorrectly formatted (e.g. reject if contains spaces) | |
258 | pattern="^[[:alnum:]]+$"; # alphanumeric string with no spaces | |
259 | if [[ $audio_format =~ $pattern ]]; then | |
260 | return_state="true"; | |
261 | # Report audio format | |
262 | echo "$audio_format"; | |
263 | else | |
264 | return_state="false"; | |
265 | fi; | |
266 | ||
267 | # Report exit code | |
268 | if [[ $return_state = "true" ]]; then | |
269 | return 0; | |
270 | else | |
271 | return 1; | |
272 | fi; | |
273 | } # Get audio format as stdout | |
274 | get_media_length() { | |
275 | # Use ffprobe to get media container length in seconds (float) | |
276 | # Usage: get_media_length arg1 | |
277 | # Input: arg1: path to file | |
278 | # Output: stdout: seconds (float) | |
279 | # Depends: ffprobe 4.1.8 | |
280 | # Ref/Attrib: [1] How to get video duration in seconds? https://superuser.com/a/945604 | |
281 | local file_in | |
282 | file_in="$1"; | |
283 | if [[ ! -f $file_in ]]; then | |
284 | die "ERROR:Not a file:$file_in"; | |
285 | fi; | |
286 | ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file_in"; | |
287 | } # Get media container length in seconds via stdout | |
288 | checkInt() { | |
289 | # Desc: Checks if arg is integer | |
290 | # Usage: checkInt arg | |
291 | # Input: arg: integer | |
292 | # Output: - return code 0 (if arg is integer) | |
293 | # - return code 1 (if arg is not integer) | |
294 | # Example: if ! checkInt $arg; then echo "not int"; fi; | |
295 | # Version: 0.0.1 | |
296 | local returnState | |
297 | ||
298 | #===Process Arg=== | |
299 | if [[ $# -ne 1 ]]; then | |
300 | die "ERROR:Invalid number of arguments:$#"; | |
301 | fi; | |
302 | ||
303 | RETEST1='^[0-9]+$'; # Regular Expression to test | |
304 | if [[ ! $1 =~ $RETEST1 ]] ; then | |
305 | returnState="false"; | |
306 | else | |
307 | returnState="true"; | |
308 | fi; | |
309 | ||
310 | #===Determine function return code=== | |
311 | if [ "$returnState" = "true" ]; then | |
312 | return 0; | |
313 | else | |
314 | return 1; | |
315 | fi; | |
316 | } # Checks if arg is integer | |
317 | checkIsInArray() { | |
318 | # Desc: Checks if input arg is element in array | |
319 | # Usage: checkIsInArray arg1 arg2 | |
320 | # Version: 0.0.1 | |
321 | # Input: arg1: test string | |
322 | # arg2: array | |
323 | # Output: exit code 0 if test string is in array; 1 otherwise | |
324 | # Example: checkIsInArray "foo" "${myArray[@]}" | |
325 | # Ref/Attrib: [1] How do I check if variable is an array? https://stackoverflow.com/a/27254437 | |
326 | # [2] How to pass an array as function argument? https://askubuntu.com/a/674347 | |
327 | local return_state input arg1 string_test | |
328 | declare -a arg2 array_test | |
329 | input=("$@") # See [2] | |
330 | arg1="${input[0]}"; | |
331 | arg2=("${input[@]:1}"); | |
332 | #yell "DEBUG:input:${input[@]}"; | |
333 | #yell "DEBUG:arg1:${arg1[@]}"; | |
334 | #yell "DEBUG:arg2:${arg2[@]}"; | |
335 | ||
336 | string_test="$arg1"; | |
337 | array_test=("${arg2[@]}"); | |
338 | ||
339 | #yell "DEBUG:string_test:$string_test"; | |
340 | #yell "DEBUG:$(declare -p array_test)"; | |
341 | for element in "${array_test[@]}"; do | |
342 | #yell "DEBUG:element:$element"; | |
343 | if [[ "$element" =~ ^"$string_test" ]]; then | |
344 | return_state="true"; | |
345 | continue; | |
346 | fi; | |
347 | done; | |
348 | ||
349 | # Report exit code | |
350 | if [[ $return_state == "true" ]]; then | |
351 | return 0; | |
352 | else | |
353 | return 1; | |
354 | fi; | |
355 | } # Check if string is element in array | |
356 | main() { | |
357 | # Desc: Main program | |
358 | # Input: arg1: path to source tree | |
359 | # arg2: path to destination tree | |
360 | # arg3: cumulative duration (seconds) of audio files in destination tree | |
361 | # assoc arrays: appRollCall, fileRollCall, dirRollCall | |
362 | # Output: [none] | |
363 | # Depends: yell(), checkdir() 0.1.2, displayMissing() 1.0.0, GNU Coreutils 8.30 (shuf) | |
364 | local arg1 arg2 arg3 dur_dest dir_source dir_dest list_all | |
365 | declare -a list_files # array for files to be considered | |
366 | declare -A list_copy # assoc array for files to be copied (key=path; value=duration) | |
367 | ||
368 | # Parse args | |
369 | arg1="$1"; | |
370 | arg2="$2"; | |
371 | arg3="$3"; | |
372 | if [[ $# -ne 3 ]]; then showUsage; die "ERROR:Invalid number of args."; fi; | |
373 | ||
374 | ## Check duration | |
375 | if checkInt "$arg3"; then | |
376 | dur_dest="$arg3"; | |
377 | else | |
378 | yell "ERROR:Duration (seconds) not an int:$arg3" | |
379 | fi; | |
380 | ||
381 | ## Check directories | |
382 | if checkdir "$arg1" "$arg2"; then | |
383 | dir_source="$arg1"; | |
384 | dir_dest="$arg2"; | |
385 | else | |
386 | yell "ERROR:Directory error"; | |
387 | fi; | |
388 | ||
389 | ## Check apps | |
390 | checkapp ffprobe; | |
391 | ||
392 | if ! displayMissing; then | |
393 | showUsage; | |
394 | die "ERROR:Check missing resources."; | |
395 | fi; | |
396 | ||
397 | yell "STATUS:Working..."; | |
398 | ||
399 | # Generate file path list | |
400 | list_all="$(find -L "$dir_source")"; | |
401 | #yell "DEBUG:list_files_rel:$list_files_rel"; | |
402 | ||
403 | # Prune list_all of non-files and save as array list_files | |
404 | while read -r line; do | |
405 | #yell "DEBUG:line:$line"; | |
406 | if ! [[ -f $line ]]; then | |
407 | #yell "DEBUG:Not a file:$line"; | |
408 | #yell ""; # debug | |
409 | continue; | |
410 | fi; | |
411 | list_files+=("$line"); | |
412 | done < <(echo "$list_all"); | |
413 | ||
414 | # Randomly test and add elements of list_files array to list_copy | |
415 | dur=0; # Initialize duration | |
416 | n=0; # Initialize loop counter | |
417 | ## Get element count of list_files array | |
418 | list_files_count="${#list_files[@]}"; | |
419 | while [[ $dur -le $dur_dest ]]; do | |
420 | #yell "DEBUG:list_copy building loop:$n"; | |
421 | ### Select random element of list_files array | |
422 | list_files_index="$(shuf -i 1-"$list_files_count" -n1)"; | |
423 | list_files_index="$((list_files_index - 1))"; # bash arrays are zero-indexed | |
424 | path_candfile="${list_files[$list_files_index]}"; # path of candidate file | |
425 | ||
426 | ### Check if has valid codec | |
427 | if ! check_parsable_audio_ffprobe "$path_candfile"; then continue; fi; # reject | |
428 | ||
429 | ### Check if desired codec | |
430 | file_format="$(get_audio_format "$path_candfile")"; | |
431 | if ! checkIsInArray "$file_format" "${music_codecs[@]}"; then continue; fi; # reject | |
432 | ||
433 | ### Check and save duration | |
434 | dur_cand="$(get_media_length "$path_candfile")"; | |
435 | dur_cand="${dur_cand%%.*}"; # convert float to int | |
436 | if ! checkInt "$dur_cand"; then continue; fi; # reject | |
40fcffff | 437 | if [[ "$dur_cand" -lt "$min_file_duration" ]]; then continue; fi; # reject |
a9a36cc4 SBS |
438 | |
439 | ### Add/update candfile to list_copy assoc. array (key=path; value=duration) | |
440 | #yell "DEBUG:Adding $path_candfile"; | |
441 | list_copy["$path_candfile"]="$dur_cand"; | |
442 | ||
443 | ### Update total duration $dur by summing all list_copy assoc. array values | |
444 | dur=0; | |
445 | for value in "${list_copy[@]}"; do | |
446 | dur="$((dur + value))"; | |
447 | done; | |
448 | #yell "DEBUG:dur:$dur"; | |
449 | ||
450 | ### Sanity check | |
451 | ((n++)); | |
452 | if [[ $n -gt $max_loops ]]; then die "ERROR:Too many loops:$n"; fi; | |
453 | done; | |
454 | ||
455 | # Copy files in list_copy to dir_dest; | |
456 | for key in "${!list_copy[@]}"; do | |
457 | value="${list_copy[$key]}"; | |
458 | ## Get basename of path | |
459 | file_basename="$(basename "$key")"; | |
460 | ||
461 | ## Get 16-character b2sum fingerprint (for different files that share basename) | |
462 | fingerprint="$(b2sum -l64 "$key" | cut -d' ' -f1)"; | |
463 | ||
40fcffff SBS |
464 | ## Form output filename |
465 | file_name="$fingerprint".."$file_basename"; | |
466 | file_name="${file_name:0:$max_filename_length}"; # Limit filename length (e.g. Windows has max of 255 characters) | |
467 | ||
a9a36cc4 | 468 | ## Form output path |
40fcffff | 469 | path_output="$dir_dest"/"$file_name"; |
a9a36cc4 SBS |
470 | |
471 | ## Copy | |
472 | try cp "$key" "$path_output" && yell "NOTICE:Copied ($value seconds): $key "; | |
473 | #yell "DEBUG:Copied $file_basename to $dur_dest."; | |
474 | ||
475 | unset file_basename path_output | |
476 | done; | |
477 | ||
478 | # Report total duration | |
479 | yell "NOTICE:Total duration (seconds):$dur"; | |
480 | ||
481 | } # Main program | |
482 | ||
483 | main "$@"; | |
484 | ||
485 | # Author: Steven Baltakatei Sandoval | |
486 | # License: GPLv3+ |