2 # Desc: Copies random audio files 
   3 # Usage: bk-copy-rand-music [dir SOURCE] [dir DEST] [int DURATION] ([int BYTES]) 
   5 # Depends: BK-2020-03: bkshuf v0.1.0 
   7 declare -Ag appRollCall 
# Associative array for storing app status 
   8 declare -Ag fileRollCall 
# Associative array for storing file status 
   9 declare -Ag dirRollCall 
# Associative array for storing dir status 
  10 declare -a music_codecs 
# Array for storing valid codec names (e.g. "aac" "mp3") 
  12 # Adjustable parameters 
  13 music_codecs
=("vorbis" "aac" "mp3" "flac" "opus"); # whitelist of valid codec_names ffprobe might return 
  14 max_filename_length
="255"; # max output filename length 
  15 min_file_duration
="10"; # minimum duration per music file 
  16 max_file_duration
="3600"; # maximum duration per music file 
  17 min_file_size
="100000"; # minimum size per music file (bytes) 
  18 max_file_size
="100000000"; # maximum size per music file (bytes) 
  19 siz_dest
="600000000"; # default destination size limit: 600 MB 
  20 max_find_depth
="10"; # max find depth 
  22 # Load env vars (bkshuf defaults for typical music albums) 
  23 if [[ ! -v BKSHUF_PARAM_LINEC 
]]; then export BKSHUF_PARAM_LINEC
=1000000; fi; 
  24 if [[ ! -v BKSHUF_PARAM_GSIZE 
]]; then export BKSHUF_PARAM_GSIZE
=10; fi; 
  26 yell
() { echo "$0: $*" >&2; } # print script path and all args to stderr 
  27 die
() { yell 
"$*"; exit 111; } # same as yell() but non-zero exit status 
  28 must
() { "$@" || die 
"cannot $*"; } # runs args as command, reports args if command fails 
  30     # Desc: If arg is a command, save result in assoc array 'appRollCall' 
  31     # Usage: checkapp arg1 arg2 arg3 ... 
  33     # Input: global assoc. array 'appRollCall' 
  34     # Output: adds/updates key(value) to global assoc array 'appRollCall' 
  40         if command -v "$arg" 1>/dev
/null 
2>&1; then # Check if arg is a valid command 
  41             appRollCall
[$arg]="true"; 
  42             if ! [ "$returnState" = "false" ]; then returnState
="true"; fi; 
  44             appRollCall
[$arg]="false"; returnState
="false"; 
  48     #===Determine function return code=== 
  49     if [ "$returnState" = "true" ]; then 
  54 } # Check that app exists 
  56     # Desc: If arg is a file path, save result in assoc array 'fileRollCall' 
  57     # Usage: checkfile arg1 arg2 arg3 ... 
  59     # Input: global assoc. array 'fileRollCall' 
  60     # Output: adds/updates key(value) to global assoc array 'fileRollCall'; 
  61     # Output: returns 0 if app found, 1 otherwise 
  67         if [ -f "$arg" ]; then 
  68             fileRollCall
["$arg"]="true"; 
  69             if ! [ "$returnState" = "false" ]; then returnState
="true"; fi; 
  71             fileRollCall
["$arg"]="false"; returnState
="false"; 
  75     #===Determine function return code=== 
  76     if [ "$returnState" = "true" ]; then 
  81 } # Check that file exists 
  83     # Desc: If arg is a dir path, save result in assoc array 'dirRollCall' 
  84     # Usage: checkdir arg1 arg2 arg3 ... 
  86     # Input: global assoc. array 'dirRollCall' 
  87     # Output: adds/updates key(value) to global assoc array 'dirRollCall'; 
  88     # Output: returns 0 if all args are dirs; 1 otherwise 
  94         if [ -z "$arg" ]; then 
  95             dirRollCall
["(Unspecified Dirname(s))"]="false"; returnState
="false"; 
  96         elif [ -d "$arg" ]; then 
  97             dirRollCall
["$arg"]="true"; 
  98             if ! [ "$returnState" = "false" ]; then returnState
="true"; fi 
 100             dirRollCall
["$arg"]="false"; returnState
="false"; 
 104     #===Determine function return code=== 
 105     if [ "$returnState" = "true" ]; then 
 110 } # Check that dir exists 
 112     # Desc: Displays missing apps, files, and dirs 
 113     # Usage: displayMissing 
 115     # Input: associative arrays: appRollCall, fileRollCall, dirRollCall 
 116     # Output: stderr: messages indicating missing apps, file, or dirs 
 117     # Output: returns exit code 0 if nothing missing; 1 otherwise 
 118     # Depends: bash 5, checkAppFileDir() 
 119     local missingApps value appMissing missingFiles fileMissing
 
 120     local missingDirs dirMissing
 
 122     #==BEGIN Display errors== 
 123     #===BEGIN Display Missing Apps=== 
 124     missingApps
="Missing apps  :"; 
 125     #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done 
 126     for key 
in "${!appRollCall[@]}"; do 
 127         value
="${appRollCall[$key]}"; 
 128         if [ "$value" = "false" ]; then 
 129             #echo "DEBUG:Missing apps: $key => $value"; 
 130             missingApps
="$missingApps""$key "; 
 134     if [ "$appMissing" = "true" ]; then  # Only indicate if an app is missing. 
 135         echo "$missingApps" 1>&2; 
 138     #===END Display Missing Apps=== 
 140     #===BEGIN Display Missing Files=== 
 141     missingFiles
="Missing files:"; 
 142     #for key in "${!fileRollCall[@]}"; do echo "DEBUG:$key => ${fileRollCall[$key]}"; done 
 143     for key 
in "${!fileRollCall[@]}"; do 
 144         value
="${fileRollCall[$key]}"; 
 145         if [ "$value" = "false" ]; then 
 146             #echo "DEBUG:Missing files: $key => $value"; 
 147             missingFiles
="$missingFiles""$key "; 
 151     if [ "$fileMissing" = "true" ]; then  # Only indicate if an app is missing. 
 152         echo "$missingFiles" 1>&2; 
 155     #===END Display Missing Files=== 
 157     #===BEGIN Display Missing Directories=== 
 158     missingDirs
="Missing dirs:"; 
 159     #for key in "${!dirRollCall[@]}"; do echo "DEBUG:$key => ${dirRollCall[$key]}"; done 
 160     for key 
in "${!dirRollCall[@]}"; do 
 161         value
="${dirRollCall[$key]}"; 
 162         if [ "$value" = "false" ]; then 
 163             #echo "DEBUG:Missing dirs: $key => $value"; 
 164             missingDirs
="$missingDirs""$key "; 
 168     if [ "$dirMissing" = "true" ]; then  # Only indicate if an dir is missing. 
 169         echo "$missingDirs" 1>&2; 
 172     #===END Display Missing Directories=== 
 174     #==END Display errors== 
 175     #==BEGIN Determine function return code=== 
 176     if [ "$appMissing" == "true" ] || 
[ "$fileMissing" == "true" ] || 
[ "$dirMissing" == "true" ]; then 
 181     #==END Determine function return code=== 
 182 } # Display missing apps, files, dirs 
 184     # Desc: Display script usage information 
 189     # Depends: GNU-coreutils 8.30 (cat) 
 193       This script may be used to copy a random selection of files containing 
 194       audio tracks from SOURCE to DEST. 
 197       bk-copy-rand-music [dir SOURCE] [dir DEST] [int DURATION] (int BYTES) 
 200       bk-copy-rand-music ~/Music /tmp/music-sample 3600 
 201       bk-copy-rand-music ~/Music /tmp/music-sample 3600 680000000 
 207     ENVIRONMENT VARIABLES 
 208       BKSHUF_PARAM_LINEC (see `bkshuf` in BK-2020-03) 
 209       BKSHUF_PARAM_GSIZE (see `bkshuf` in BK-2020-03) 
 211 } # Display information on how to use this script. 
 212 check_parsable_audio_ffprobe
() { 
 213     # Desc: Checks if ffprobe returns valid audio codec name for file 
 214     # Usage: check_parsable_audio_ffprobe [path FILE] 
 216     # Input: arg1: file path 
 217     # Output: exit code 0 if returns valid codec name; 1 otherwise 
 218     # Depends: ffprobe, die() 
 219     local file_in ffprobe_out
 
 221     if [[ $# -ne 1 ]]; then die 
"ERROR:Invalid number of args:$#"; fi; 
 225     # Check if ffprobe detects an audio stream 
 226     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 
 229         return_state
="false"; 
 232     # Fail if ffprobe returns no result 
 233     ffprobe_out
="$(ffprobe -v error -select_streams a -show_entries stream=codec_name -of default=nokey=1:noprint_wrappers=1 "$file_in")"; 
 234     if [[ -z $ffprobe_out ]]; then 
 235         return_state
="false"; 
 239     if [[ $return_state = "true" ]]; then 
 244 } # Checks if file has valid codec name using ffprobe 
 246     # Desc: Gets audio format of file as string 
 247     # Usage: get_audio_format arg1 
 250     # Input: arg1: input file path 
 251     # Output: stdout (if valid audio format) 
 252     #         exit code 0 if audio file; 1 otherwise 
 253     # Example: get_audio_format myvideo.mp4 
 254     #   Note: Would return "opus" if full ffprobe report had 'Audio: opus, 48000 Hz, stereo, fltp' 
 255     # Note: Not tested with videos containing multiple video streams 
 256     # Ref/Attrib: [1] https://stackoverflow.com/questions/5618363/is-there-a-way-to-use-ffmpeg-to-determine-the-encoding-of-a-file-before-transcod 
 257     #             [2] https://stackoverflow.com/questions/44123532/how-to-find-out-the-file-extension-for-extracting-audio-tracks-with-ffmpeg-and-p#comment88464070_50723126 
 258     local audio_format file_in
; 
 262     # Return error exit code if not audio file 
 263     ## Return error if ffprobe itself exited on error 
 264     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 
 265         return_state
="false"; 
 269     audio_format
="$(ffprobe -v error -select_streams a -show_entries stream=codec_name -of default=nokey=1:noprint_wrappers=1 "$file_in")"; # see [1] 
 271     ## Return error if audio format is incorrectly formatted (e.g. reject if contains spaces) 
 272     pattern
="^[[:alnum:]]+$"; # alphanumeric string with no spaces 
 273     if [[ $audio_format =~ 
$pattern ]]; then 
 275         # Report audio format 
 276         echo "$audio_format"; 
 278         return_state
="false"; 
 282     if [[ $return_state = "true" ]]; then 
 287 } # Get audio format as stdout 
 289     # Use ffprobe to get media container length in seconds (float) 
 290     # Usage: get_media_length arg1 
 291     # Input:  arg1: path to file 
 292     # Output: stdout: seconds (float) 
 293     # Depends: ffprobe 4.1.8 
 294     # Ref/Attrib: [1] How to get video duration in seconds? https://superuser.com/a/945604 
 297     if [[ ! -f $file_in ]]; then 
 298         die 
"ERROR:Not a file:$file_in"; 
 300     ffprobe 
-v error 
-show_entries format
=duration 
-of default
=noprint_wrappers
=1:nokey
=1 "$file_in"; 
 301 } # Get media container length in seconds via stdout 
 303     # Desc: Checks if arg is integer 
 304     # Usage: checkInt arg 
 305     # Input: arg: integer 
 306     # Output: - return code 0 (if arg is integer) 
 307     #         - return code 1 (if arg is not integer) 
 308     # Example: if ! checkInt $arg; then echo "not int"; fi; 
 313     if [[ $# -ne 1 ]]; then 
 314         die 
"ERROR:Invalid number of arguments:$#"; 
 317     RETEST1
='^[0-9]+$'; # Regular Expression to test 
 318     if [[ ! $1 =~ 
$RETEST1 ]] ; then 
 324     #===Determine function return code=== 
 325     if [ "$returnState" = "true" ]; then 
 330 } # Checks if arg is integer 
 332     # Desc: Checks if input arg is element in array 
 333     # Usage: checkIsInArray arg1 arg2 
 335     # Input: arg1: test string 
 337     # Output: exit code 0 if test string is in array; 1 otherwise 
 338     # Example: checkIsInArray "foo" "${myArray[@]}" 
 339     # Ref/Attrib: [1] How do I check if variable is an array? https://stackoverflow.com/a/27254437 
 340     #             [2] How to pass an array as function argument? https://askubuntu.com/a/674347 
 341     local return_state input arg1 string_test
 
 342     declare -a arg2 array_test
 
 343     input
=("$@") # See [2] 
 345     arg2
=("${input[@]:1}"); 
 346     #yell "DEBUG:input:${input[@]}"; 
 347     #yell "DEBUG:arg1:${arg1[@]}"; 
 348     #yell "DEBUG:arg2:${arg2[@]}"; 
 351     array_test
=("${arg2[@]}"); 
 353     #yell "DEBUG:string_test:$string_test"; 
 354     #yell "DEBUG:$(declare -p array_test)"; 
 355     for element 
in "${array_test[@]}"; do 
 356         #yell "DEBUG:element:$element"; 
 357         if [[ "$element" =~ ^
"$string_test" ]]; then 
 364     if [[ $return_state == "true" ]]; then 
 369 } # Check if string is element in array 
 372     # Input: arg1: path to source tree 
 373     #        arg2: path to destination tree 
 374     #        arg3: cumulative duration (seconds) of audio files in destination tree 
 375     #        arg4: cumulative size (bytes) of audio files in destination tree (optional) 
 376     #        assoc arrays: appRollCall, fileRollCall, dirRollCall 
 377     #        env.var: BKSHUF_PARAM_LINEC (bkshuf) 
 378     #                 BKSHUF_PARAM_GSIZE (bkshuf) 
 379     #        arrays: music_codecs 
 380     #        vars: max_filename_length, min_file_duration, max_file_duration, 
 381     #                min_file_size, max_file_size, siz_dest, max_find_depth 
 383     # Depends: yell(), checkdir() 0.1.2, displayMissing() 1.0.0, GNU Coreutils 8.30 
 384     #          BK-2020-03: bkshuf v0.1.0 
 385     local arg1 arg2 arg3 dur_dest dir_source dir_dest
 
 386     declare -a list_files 
# array for files to be considered 
 387     declare -a list_copy 
# array for files to be copied (string: "$dur,$fsize,$path") 
 394     if ! { [[ $# -eq 3 ]] || 
[[ $# -eq 4 ]]; }; then 
 396         die 
"ERROR:Invalid number of args:$#"; fi; 
 399     if ! checkInt 
"$BKSHUF_PARAM_LINEC"; then 
 400         die 
"FATAL:Not an int:BKSHUF_PARAM_LINEC:$BKSHUF_PARAM_LINEC"; fi; 
 401     if ! checkInt 
"$BKSHUF_PARAM_GSIZE"; then 
 402         die 
"FATAL:Not an int:BKSHUF_PARAM_LINEC:$BKSHUF_PARAM_GSIZE"; fi; 
 405     if checkInt 
"$arg3"; then 
 408         die 
"FATAL:Duration (seconds) not an int:$arg3" 
 412     if [[ -n "$arg4" ]]; then 
 413         if checkInt 
"$arg4"; then 
 416             die 
"FATAL:Size (bytes) not an int:$arg4"; 
 421     if checkdir 
"$arg1" "$arg2"; then 
 425         yell 
"ERROR:Directory error"; 
 429     checkapp ffprobe bkshuf
; 
 431     if ! displayMissing
; then 
 433         die 
"ERROR:Check missing resources."; 
 436     yell 
"STATUS:Working..."; 
 438     # Populate list_files array 
 439     while read -r line
; do 
 440         list_files
+=("$line"); 
 441     done < <(find -L "$dir_source" -maxdepth "$max_find_depth" -type f | 
sort); 
 443     # Test and add random elements of list_files to list_copy 
 444     dur
=0; # Initialize duration 
 445     siz
=0; # Initialize size 
 446     n
=0; # Initialize loop counter 
 447     dur_cand_w
=1; # Init duration digit width counter 
 448     siz_cand_w
=1; # Init size digit width counter 
 449     ## Get element count of list_files array 
 450     file_count
="${#list_files[@]}"; 
 451     while read -r line 
&& \
 
 452             [[ $dur -le $dur_dest ]] && \
 
 453             [[ $siz -le $siz_dest ]] && \
 
 454             [[ $n -le $file_count ]]; do 
 455         #yell "DEBUG:list_copy building loop:$n"; 
 456         path_candfile
="$line"; # path of candidate file 
 458         ### Check if has valid codec 
 459         if ! check_parsable_audio_ffprobe 
"$path_candfile"; then continue; fi; # reject 
 461         ### Check if desired codec 
 462         file_format
="$(get_audio_format "$path_candfile")"; 
 463         if ! checkIsInArray 
"$file_format" "${music_codecs[@]}"; then continue; fi; # reject 
 465         ### Check and save duration 
 466         dur_cand
="$(get_media_length "$path_candfile")"; 
 467         dur_cand
="${dur_cand%%.*}"; # convert float to int 
 468         if [[ "$((dur + dur_cand))" -gt "$dur_dest" ]]; then break; fi; # no more 
 469         dur_cand_wnow
="$(printf "%s
" "$dur_cand" | wc -m)"; # duration width count 
 470         if [[ $dur_cand_wnow -gt $dur_cand_w ]]; then 
 471             dur_cand_w
="$dur_cand_wnow"; fi; 
 472         if ! checkInt 
"$dur_cand"; then continue; fi; # reject 
 473         if [[ "$dur_cand" -lt "$min_file_duration" ]]; then continue; fi; # reject 
 474         if [[ "$dur_cand" -gt "$max_file_duration" ]]; then continue; fi; # reject 
 476         ### Check and save size 
 477         siz_cand
="$(du -b "$path_candfile" | awk '{ print $1 }')"; # size in bytes 
 478         if [[ "$((siz + siz_cand))" -gt "$siz_dest" ]]; then break; fi; # no more 
 479         siz_cand_wnow
="$(printf "%s
" "$siz_cand" | wc -m)"; # size  width count 
 480         if [[ $siz_cand_wnow -gt $siz_cand_w ]]; then 
 481             siz_cand_w
="$siz_cand_wnow"; fi; 
 482         if ! checkInt 
"$siz_cand"; then continue; fi; # reject 
 483         if [[ "$siz_cand" -lt "$min_file_size" ]]; then continue; fi; # reject 
 484         if [[ "$siz_cand" -gt "$max_file_size" ]]; then continue; fi; # reject 
 486         ### Add/update candfile to array: 
 487         ###   list_copy (array with "duration, size, path") 
 488         #yell "DEBUG:Adding $path_candfile"; 
 489         #printf "DEBUG:%8d,%8d,%s\n" "$dur_cand" "$siz_cand" "$path_candfile" 1>&2; 
 490         #printf "DEBUG:dur:%s\n" "$dur" 1>&2; 
 491         #printf "DEBUG:siz:%s\n" "$siz" 1>&2; 
 492         list_copy
+=("$dur_cand,$siz_cand,$path_candfile"); # for copying with order 
 494         ### Update total duration $dur and total size $siz 
 495         dur
="$((dur + dur_cand))"; 
 496         siz
="$((siz + siz_cand))"; 
 497         #yell "DEBUG:dur:$dur"; 
 498         #yell "DEBUG:siz:$siz"; 
 501     done < <(printf "%s\n" "${list_files[@]}" | bkshuf
); 
 503     #yell "DEBUG:BKSHUF_PARAM_LINEC:$BKSHUF_PARAM_LINEC"; 
 504     #yell "DEBUG:BKSHUF_PARAM_GSIZE:$BKSHUF_PARAM_GSIZE"; 
 506     n
=0; # Initialize loop counter 
 507     num_w
="$(printf "%s
" "${#list_copy[@]}" | wc -m)"; # init file number format 
 508     num_fmt
="%0""$num_w""d"; 
 509     path_log_output
="$dir_dest"/COPY.log
; 
 510     printf "num,fingerprint,duration,size,original_path\n" >> "$path_log_output"; 
 511     # Copy files in list_copy to dir_dest; 
 512     while read -r line
; do 
 513         #yell "DEBUG:line:$line"; # debug 
 514         fdur
="$(printf "%s
" "$line" | cut -d',' -f1)"; 
 515         fsize
="$(printf "%s
" "$line" | cut -d',' -f2)"; 
 516         fpath
="$(printf "%s
" "$line" | cut -d',' -f3-)"; 
 517         ## Get basename of path 
 518         file_basename
="$(basename "$fpath")"; 
 520         ## Get 16-character b2sum fingerprint (for different files that share basename) 
 521         fingerprint
="$(b2sum -l32 "$fpath" | awk '{print $1}' )"; 
 523         ## Form output filename 
 524         num
="$(printf "$num_fmt" "$n")"; 
 525         file_name
="$num"_
"$fingerprint"..
"$file_basename"; 
 526         file_name
="${file_name:0:$max_filename_length}"; # Limit filename length (e.g. Windows has max of 255 characters) 
 529         path_output
="$dir_dest"/"$file_name"; 
 532         must 
cp "$fpath" "$path_output" && yell 
"NOTICE:Copied ($fdur seconds): $fpath "; 
 533         #yell "DEBUG:Copied $file_basename to $dur_dest."; 
 536         fpath_can
="$(readlink -f "$fpath")"; # resolve symlinks to canonical path 
 537         log_fmt
="%s,%s,%""$dur_cand_w""d,%""$siz_cand_w""d,%s\n"; # e.g. "%s,%3d,%5d,%s" if dur_cand_w=3 and siz_cand_w=5 
 538         printf "$log_fmt" "$num" "$fingerprint" "$fdur" "$fsize" "$fpath_can" >> "$path_log_output"; 
 541         unset file_basename path_output
 
 542     done < <(printf "%s\n" "${list_copy[@]}"); 
 544     # Report total duration and size 
 545     yell 
"NOTICE:Total duration (seconds):$dur"; 
 546     yell 
"NOTICE:Total size (bytes):$siz"; 
 552 # Author: Steven Baltakatei Sandoval 
 556 #   Author: Steven Baltakatei Sandoval 
 558 #   URL: https://gitlab.com/baltakatei/baltakatei-exdev/-/blob/b9e8b771e985fcdf26ba8b9ccb8e31b62da757d3/unitproc/bkshuf