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