2 # Desc: Copies random music 
   3 # Usage: bk-copy-rand-music.sh [dir SOURCE] [dir DEST] [int DURATION] 
   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") 
  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 
  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 
  18     # Desc: If arg is a command, save result in assoc array 'appRollCall' 
  19     # Usage: checkapp arg1 arg2 arg3 ... 
  21     # Input: global assoc. array 'appRollCall' 
  22     # Output: adds/updates key(value) to global assoc array 'appRollCall' 
  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; 
  32             appRollCall
[$arg]="false"; returnState
="false"; 
  36     #===Determine function return code=== 
  37     if [ "$returnState" = "true" ]; then 
  42 } # Check that app exists 
  44     # Desc: If arg is a file path, save result in assoc array 'fileRollCall' 
  45     # Usage: checkfile arg1 arg2 arg3 ... 
  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 
  55         if [ -f "$arg" ]; then 
  56             fileRollCall
["$arg"]="true"; 
  57             if ! [ "$returnState" = "false" ]; then returnState
="true"; fi; 
  59             fileRollCall
["$arg"]="false"; returnState
="false"; 
  63     #===Determine function return code=== 
  64     if [ "$returnState" = "true" ]; then 
  69 } # Check that file exists 
  71     # Desc: If arg is a dir path, save result in assoc array 'dirRollCall' 
  72     # Usage: checkdir arg1 arg2 arg3 ... 
  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 
  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 
  88             dirRollCall
["$arg"]="false"; returnState
="false"; 
  92     #===Determine function return code=== 
  93     if [ "$returnState" = "true" ]; then 
  98 } # Check that dir exists 
 100     # Desc: Displays missing apps, files, and dirs 
 101     # Usage: displayMissing 
 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
 
 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 "; 
 122     if [ "$appMissing" = "true" ]; then  # Only indicate if an app is missing. 
 123         echo "$missingApps" 1>&2; 
 126     #===END Display Missing Apps=== 
 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 "; 
 139     if [ "$fileMissing" = "true" ]; then  # Only indicate if an app is missing. 
 140         echo "$missingFiles" 1>&2; 
 143     #===END Display Missing Files=== 
 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 "; 
 156     if [ "$dirMissing" = "true" ]; then  # Only indicate if an dir is missing. 
 157         echo "$missingDirs" 1>&2; 
 160     #===END Display Missing Directories=== 
 162     #==END Display errors== 
 163     #==BEGIN Determine function return code=== 
 164     if [ "$appMissing" == "true" ] || 
[ "$fileMissing" == "true" ] || 
[ "$dirMissing" == "true" ]; then 
 169     #==END Determine function return code=== 
 170 } # Display missing apps, files, dirs 
 172     # Desc: Display script usage information 
 177     # Depends: GNU-coreutils 8.30 (cat) 
 181       This script may be used to copy a random selection of files from 
 185       bk-copy-rand-music [dir SOURCE] [dir DEST] [int DURATION] 
 188       bk-copy-rand-music ~/Music /tmp/music-sample 3600 
 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] 
 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
 
 204     if [[ $# -ne 1 ]]; then die 
"ERROR:Invalid number of args:$#"; fi; 
 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 
 212         return_state
="false"; 
 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"; 
 222     if [[ $return_state = "true" ]]; then 
 227 } # Checks if file has valid codec name using ffprobe 
 229     # Desc: Gets audio format of file as string 
 230     # Usage: get_audio_format arg1 
 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
; 
 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"; 
 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] 
 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 
 258         # Report audio format 
 259         echo "$audio_format"; 
 261         return_state
="false"; 
 265     if [[ $return_state = "true" ]]; then 
 270 } # Get audio format as stdout 
 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 
 280     if [[ ! -f $file_in ]]; then 
 281         die 
"ERROR:Not a file:$file_in"; 
 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 
 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; 
 296     if [[ $# -ne 1 ]]; then 
 297         die 
"ERROR:Invalid number of arguments:$#"; 
 300     RETEST1
='^[0-9]+$'; # Regular Expression to test 
 301     if [[ ! $1 =~ 
$RETEST1 ]] ; then 
 307     #===Determine function return code=== 
 308     if [ "$returnState" = "true" ]; then 
 313 } # Checks if arg is integer 
 315     # Desc: Checks if input arg is element in array 
 316     # Usage: checkIsInArray arg1 arg2 
 318     # Input: arg1: test string 
 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] 
 328     arg2
=("${input[@]:1}"); 
 329     #yell "DEBUG:input:${input[@]}"; 
 330     #yell "DEBUG:arg1:${arg1[@]}"; 
 331     #yell "DEBUG:arg2:${arg2[@]}"; 
 334     array_test
=("${arg2[@]}"); 
 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 
 347     if [[ $return_state == "true" ]]; then 
 352 } # Check if string is element in array 
 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 
 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) 
 369     if [[ $# -ne 3 ]]; then showUsage
; die 
"ERROR:Invalid number of args."; fi; 
 372     if checkInt 
"$arg3"; then 
 375         yell 
"ERROR:Duration (seconds) not an int:$arg3" 
 379     if checkdir 
"$arg1" "$arg2"; then 
 383         yell 
"ERROR:Directory error"; 
 389     if ! displayMissing
; then 
 391         die 
"ERROR:Check missing resources."; 
 394     yell 
"STATUS:Working..."; 
 396     # Generate file path list 
 397     list_all
="$(find -L "$dir_source")"; 
 398     #yell "DEBUG:list_files_rel:$list_files_rel"; 
 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"; 
 408         list_files
+=("$line"); 
 409     done < <(echo "$list_all"); 
 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 
 423         ### Check if has valid codec 
 424         if ! check_parsable_audio_ffprobe 
"$path_candfile"; then continue; fi; # reject 
 426         ### Check if desired codec 
 427         file_format
="$(get_audio_format "$path_candfile")"; 
 428         if ! checkIsInArray 
"$file_format" "${music_codecs[@]}"; then continue; fi; # reject 
 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 
 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"; 
 439         ### Update total duration $dur by summing all list_copy assoc. array values 
 441         for value 
in "${list_copy[@]}"; do 
 442             dur
="$((dur + value))"; 
 444         #yell "DEBUG:dur:$dur"; 
 448         if [[ $n -gt $max_loops ]]; then die 
"ERROR:Too many loops:$n"; fi; 
 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")"; 
 457         ## Get 16-character b2sum fingerprint (for different files that share basename) 
 458         fingerprint
="$(b2sum -l64 "$key" | cut -d' ' -f1)"; 
 461         path_output
="$dir_dest"/"$fingerprint"..
"$file_basename"; 
 464         try 
cp "$key" "$path_output" && yell 
"NOTICE:Copied ($value seconds): $key "; 
 465         #yell "DEBUG:Copied $file_basename to $dur_dest."; 
 467         unset file_basename path_output
 
 470     # Report total duration 
 471     yell 
"NOTICE:Total duration (seconds):$dur"; 
 477 # Author: Steven Baltakatei Sandoval