+++ /dev/null
-#!/usr/bin/env bash
-# Desc: Copies random audio files
-# Usage: bk-copy-rand-music.sh [dir SOURCE] [dir DEST] [int DURATION]
-# Version: 0.0.3
-
-declare -Ag appRollCall # Associative array for storing app status
-declare -Ag fileRollCall # Associative array for storing file status
-declare -Ag dirRollCall # Associative array for storing dir status
-declare -a music_codecs # Array for storing valid codec names (e.g. "aac" "mp3")
-
-# Adjustable parameters
-music_codecs=("vorbis" "aac" "mp3" "flac" "opus"); # whitelist of valid codec_names ffprobe might return
-max_loops="1000000"; # max number of files to test whether are audio or not
-max_filename_length="255"; # max output filename length
-min_file_duration="10"; # minimum duration per music file
-
-yell() { echo "$0: $*" >&2; } # print script path and all args to stderr
-die() { yell "$*"; exit 111; } # same as yell() but non-zero exit status
-try() { "$@" || die "cannot $*"; } # runs args as command, reports args if command fails
-checkapp() {
- # Desc: If arg is a command, save result in assoc array 'appRollCall'
- # Usage: checkapp arg1 arg2 arg3 ...
- # Version: 0.1.1
- # Input: global assoc. array 'appRollCall'
- # Output: adds/updates key(value) to global assoc array 'appRollCall'
- # Depends: bash 5.0.3
- local returnState
-
- #===Process Args===
- for arg in "$@"; do
- if command -v "$arg" 1>/dev/null 2>&1; then # Check if arg is a valid command
- appRollCall[$arg]="true";
- if ! [ "$returnState" = "false" ]; then returnState="true"; fi;
- else
- appRollCall[$arg]="false"; returnState="false";
- fi;
- done;
-
- #===Determine function return code===
- if [ "$returnState" = "true" ]; then
- return 0;
- else
- return 1;
- fi;
-} # Check that app exists
-checkfile() {
- # Desc: If arg is a file path, save result in assoc array 'fileRollCall'
- # Usage: checkfile arg1 arg2 arg3 ...
- # Version: 0.1.1
- # Input: global assoc. array 'fileRollCall'
- # Output: adds/updates key(value) to global assoc array 'fileRollCall';
- # Output: returns 0 if app found, 1 otherwise
- # Depends: bash 5.0.3
- local returnState
-
- #===Process Args===
- for arg in "$@"; do
- if [ -f "$arg" ]; then
- fileRollCall["$arg"]="true";
- if ! [ "$returnState" = "false" ]; then returnState="true"; fi;
- else
- fileRollCall["$arg"]="false"; returnState="false";
- fi;
- done;
-
- #===Determine function return code===
- if [ "$returnState" = "true" ]; then
- return 0;
- else
- return 1;
- fi;
-} # Check that file exists
-checkdir() {
- # Desc: If arg is a dir path, save result in assoc array 'dirRollCall'
- # Usage: checkdir arg1 arg2 arg3 ...
- # Version 0.1.2
- # Input: global assoc. array 'dirRollCall'
- # Output: adds/updates key(value) to global assoc array 'dirRollCall';
- # Output: returns 0 if all args are dirs; 1 otherwise
- # Depends: Bash 5.0.3
- local returnState
-
- #===Process Args===
- for arg in "$@"; do
- if [ -z "$arg" ]; then
- dirRollCall["(Unspecified Dirname(s))"]="false"; returnState="false";
- elif [ -d "$arg" ]; then
- dirRollCall["$arg"]="true";
- if ! [ "$returnState" = "false" ]; then returnState="true"; fi
- else
- dirRollCall["$arg"]="false"; returnState="false";
- fi
- done
-
- #===Determine function return code===
- if [ "$returnState" = "true" ]; then
- return 0;
- else
- return 1;
- fi
-} # Check that dir exists
-displayMissing() {
- # Desc: Displays missing apps, files, and dirs
- # Usage: displayMissing
- # Version 1.0.0
- # Input: associative arrays: appRollCall, fileRollCall, dirRollCall
- # Output: stderr: messages indicating missing apps, file, or dirs
- # Output: returns exit code 0 if nothing missing; 1 otherwise
- # Depends: bash 5, checkAppFileDir()
- local missingApps value appMissing missingFiles fileMissing
- local missingDirs dirMissing
-
- #==BEGIN Display errors==
- #===BEGIN Display Missing Apps===
- missingApps="Missing apps :";
- #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done
- for key in "${!appRollCall[@]}"; do
- value="${appRollCall[$key]}";
- if [ "$value" = "false" ]; then
- #echo "DEBUG:Missing apps: $key => $value";
- missingApps="$missingApps""$key ";
- appMissing="true";
- fi;
- done;
- if [ "$appMissing" = "true" ]; then # Only indicate if an app is missing.
- echo "$missingApps" 1>&2;
- fi;
- unset value;
- #===END Display Missing Apps===
-
- #===BEGIN Display Missing Files===
- missingFiles="Missing files:";
- #for key in "${!fileRollCall[@]}"; do echo "DEBUG:$key => ${fileRollCall[$key]}"; done
- for key in "${!fileRollCall[@]}"; do
- value="${fileRollCall[$key]}";
- if [ "$value" = "false" ]; then
- #echo "DEBUG:Missing files: $key => $value";
- missingFiles="$missingFiles""$key ";
- fileMissing="true";
- fi;
- done;
- if [ "$fileMissing" = "true" ]; then # Only indicate if an app is missing.
- echo "$missingFiles" 1>&2;
- fi;
- unset value;
- #===END Display Missing Files===
-
- #===BEGIN Display Missing Directories===
- missingDirs="Missing dirs:";
- #for key in "${!dirRollCall[@]}"; do echo "DEBUG:$key => ${dirRollCall[$key]}"; done
- for key in "${!dirRollCall[@]}"; do
- value="${dirRollCall[$key]}";
- if [ "$value" = "false" ]; then
- #echo "DEBUG:Missing dirs: $key => $value";
- missingDirs="$missingDirs""$key ";
- dirMissing="true";
- fi;
- done;
- if [ "$dirMissing" = "true" ]; then # Only indicate if an dir is missing.
- echo "$missingDirs" 1>&2;
- fi;
- unset value;
- #===END Display Missing Directories===
-
- #==END Display errors==
- #==BEGIN Determine function return code===
- if [ "$appMissing" == "true" ] || [ "$fileMissing" == "true" ] || [ "$dirMissing" == "true" ]; then
- return 1;
- else
- return 0;
- fi
- #==END Determine function return code===
-} # Display missing apps, files, dirs
-showUsage() {
- # Desc: Display script usage information
- # Usage: showUsage
- # Version 0.0.1
- # Input: none
- # Output: stdout
- # Depends: GNU-coreutils 8.30 (cat)
- cat <<'EOF'
-
- DESCRIPTION:
- This script may be used to copy a random selection of files containing
- audio tracks from SOURCE to DEST.
-
- USAGE:
- bk-copy-rand-music [dir SOURCE] [dir DEST] [int DURATION]
-
- EXAMPLE:
- bk-copy-rand-music ~/Music /tmp/music-sample 3600
-
- DEPENDENCIES:
- ffprobe
- GNU Coreutils 8.30
-EOF
-} # Display information on how to use this script.
-check_parsable_audio_ffprobe() {
- # Desc: Checks if ffprobe returns valid audio codec name for file
- # Usage: check_parsable_audio_ffprobe [path FILE]
- # Version: 0.0.1
- # Input: arg1: file path
- # Output: exit code 0 if returns valid codec name; 1 otherwise
- # Depends: ffprobe, die()
- local file_in ffprobe_out
-
- if [[ $# -ne 1 ]]; then die "ERROR:Invalid number of args:$#"; fi;
-
- file_in="$1";
-
- # Check if ffprobe detects an audio stream
- 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
- return_state="true";
- else
- return_state="false";
- fi;
-
- # Fail if ffprobe returns no result
- ffprobe_out="$(ffprobe -v error -select_streams a -show_entries stream=codec_name -of default=nokey=1:noprint_wrappers=1 "$file_in")";
- if [[ -z $ffprobe_out ]]; then
- return_state="false";
- fi;
-
- # Report exit code
- if [[ $return_state = "true" ]]; then
- return 0;
- else
- return 1;
- fi;
-} # Checks if file has valid codec name using ffprobe
-get_audio_format() {
- # Desc: Gets audio format of file as string
- # Usage: get_audio_format arg1
- # Depends: ffprobe
- # Version: 0.0.1
- # Input: arg1: input file path
- # Output: stdout (if valid audio format)
- # exit code 0 if audio file; 1 otherwise
- # Example: get_audio_format myvideo.mp4
- # Note: Would return "opus" if full ffprobe report had 'Audio: opus, 48000 Hz, stereo, fltp'
- # Note: Not tested with videos containing multiple video streams
- # Ref/Attrib: [1] https://stackoverflow.com/questions/5618363/is-there-a-way-to-use-ffmpeg-to-determine-the-encoding-of-a-file-before-transcod
- # [2] https://stackoverflow.com/questions/44123532/how-to-find-out-the-file-extension-for-extracting-audio-tracks-with-ffmpeg-and-p#comment88464070_50723126
- local audio_format file_in;
- local return_state;
- file_in="$1";
-
- # Return error exit code if not audio file
- ## Return error if ffprobe itself exited on error
- 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
- return_state="false";
- fi;
-
- # Get audio format
- audio_format="$(ffprobe -v error -select_streams a -show_entries stream=codec_name -of default=nokey=1:noprint_wrappers=1 "$file_in")"; # see [1]
-
- ## Return error if audio format is incorrectly formatted (e.g. reject if contains spaces)
- pattern="^[[:alnum:]]+$"; # alphanumeric string with no spaces
- if [[ $audio_format =~ $pattern ]]; then
- return_state="true";
- # Report audio format
- echo "$audio_format";
- else
- return_state="false";
- fi;
-
- # Report exit code
- if [[ $return_state = "true" ]]; then
- return 0;
- else
- return 1;
- fi;
-} # Get audio format as stdout
-get_media_length() {
- # Use ffprobe to get media container length in seconds (float)
- # Usage: get_media_length arg1
- # Input: arg1: path to file
- # Output: stdout: seconds (float)
- # Depends: ffprobe 4.1.8
- # Ref/Attrib: [1] How to get video duration in seconds? https://superuser.com/a/945604
- local file_in
- file_in="$1";
- if [[ ! -f $file_in ]]; then
- die "ERROR:Not a file:$file_in";
- fi;
- ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file_in";
-} # Get media container length in seconds via stdout
-checkInt() {
- # Desc: Checks if arg is integer
- # Usage: checkInt arg
- # Input: arg: integer
- # Output: - return code 0 (if arg is integer)
- # - return code 1 (if arg is not integer)
- # Example: if ! checkInt $arg; then echo "not int"; fi;
- # Version: 0.0.1
- local returnState
-
- #===Process Arg===
- if [[ $# -ne 1 ]]; then
- die "ERROR:Invalid number of arguments:$#";
- fi;
-
- RETEST1='^[0-9]+$'; # Regular Expression to test
- if [[ ! $1 =~ $RETEST1 ]] ; then
- returnState="false";
- else
- returnState="true";
- fi;
-
- #===Determine function return code===
- if [ "$returnState" = "true" ]; then
- return 0;
- else
- return 1;
- fi;
-} # Checks if arg is integer
-checkIsInArray() {
- # Desc: Checks if input arg is element in array
- # Usage: checkIsInArray arg1 arg2
- # Version: 0.0.1
- # Input: arg1: test string
- # arg2: array
- # Output: exit code 0 if test string is in array; 1 otherwise
- # Example: checkIsInArray "foo" "${myArray[@]}"
- # Ref/Attrib: [1] How do I check if variable is an array? https://stackoverflow.com/a/27254437
- # [2] How to pass an array as function argument? https://askubuntu.com/a/674347
- local return_state input arg1 string_test
- declare -a arg2 array_test
- input=("$@") # See [2]
- arg1="${input[0]}";
- arg2=("${input[@]:1}");
- #yell "DEBUG:input:${input[@]}";
- #yell "DEBUG:arg1:${arg1[@]}";
- #yell "DEBUG:arg2:${arg2[@]}";
-
- string_test="$arg1";
- array_test=("${arg2[@]}");
-
- #yell "DEBUG:string_test:$string_test";
- #yell "DEBUG:$(declare -p array_test)";
- for element in "${array_test[@]}"; do
- #yell "DEBUG:element:$element";
- if [[ "$element" =~ ^"$string_test" ]]; then
- return_state="true";
- continue;
- fi;
- done;
-
- # Report exit code
- if [[ $return_state == "true" ]]; then
- return 0;
- else
- return 1;
- fi;
-} # Check if string is element in array
-main() {
- # Desc: Main program
- # Input: arg1: path to source tree
- # arg2: path to destination tree
- # arg3: cumulative duration (seconds) of audio files in destination tree
- # assoc arrays: appRollCall, fileRollCall, dirRollCall
- # Output: [none]
- # Depends: yell(), checkdir() 0.1.2, displayMissing() 1.0.0, GNU Coreutils 8.30 (shuf)
- local arg1 arg2 arg3 dur_dest dir_source dir_dest list_all
- declare -a list_files # array for files to be considered
- declare -A list_copy # assoc array for files to be copied (key=path; value=duration)
-
- # Parse args
- arg1="$1";
- arg2="$2";
- arg3="$3";
- if [[ $# -ne 3 ]]; then showUsage; die "ERROR:Invalid number of args."; fi;
-
- ## Check duration
- if checkInt "$arg3"; then
- dur_dest="$arg3";
- else
- yell "ERROR:Duration (seconds) not an int:$arg3"
- fi;
-
- ## Check directories
- if checkdir "$arg1" "$arg2"; then
- dir_source="$arg1";
- dir_dest="$arg2";
- else
- yell "ERROR:Directory error";
- fi;
-
- ## Check apps
- checkapp ffprobe;
-
- if ! displayMissing; then
- showUsage;
- die "ERROR:Check missing resources.";
- fi;
-
- yell "STATUS:Working...";
-
- # Generate file path list
- list_all="$(find -L "$dir_source")";
- #yell "DEBUG:list_files_rel:$list_files_rel";
-
- # Prune list_all of non-files and save as array list_files
- while read -r line; do
- #yell "DEBUG:line:$line";
- if ! [[ -f $line ]]; then
- #yell "DEBUG:Not a file:$line";
- #yell ""; # debug
- continue;
- fi;
- list_files+=("$line");
- done < <(echo "$list_all");
-
- # Randomly test and add elements of list_files array to list_copy
- dur=0; # Initialize duration
- n=0; # Initialize loop counter
- ## Get element count of list_files array
- list_files_count="${#list_files[@]}";
- while [[ $dur -le $dur_dest ]]; do
- #yell "DEBUG:list_copy building loop:$n";
- ### Select random element of list_files array
- list_files_index="$(shuf -i 1-"$list_files_count" -n1)";
- list_files_index="$((list_files_index - 1))"; # bash arrays are zero-indexed
- path_candfile="${list_files[$list_files_index]}"; # path of candidate file
-
- ### Check if has valid codec
- if ! check_parsable_audio_ffprobe "$path_candfile"; then continue; fi; # reject
-
- ### Check if desired codec
- file_format="$(get_audio_format "$path_candfile")";
- if ! checkIsInArray "$file_format" "${music_codecs[@]}"; then continue; fi; # reject
-
- ### Check and save duration
- dur_cand="$(get_media_length "$path_candfile")";
- dur_cand="${dur_cand%%.*}"; # convert float to int
- if ! checkInt "$dur_cand"; then continue; fi; # reject
- if [[ "$dur_cand" -lt "$min_file_duration" ]]; then continue; fi; # reject
-
- ### Add/update candfile to list_copy assoc. array (key=path; value=duration)
- #yell "DEBUG:Adding $path_candfile";
- list_copy["$path_candfile"]="$dur_cand";
-
- ### Update total duration $dur by summing all list_copy assoc. array values
- dur=0;
- for value in "${list_copy[@]}"; do
- dur="$((dur + value))";
- done;
- #yell "DEBUG:dur:$dur";
-
- ### Sanity check
- ((n++));
- if [[ $n -gt $max_loops ]]; then die "ERROR:Too many loops:$n"; fi;
- done;
-
- n=0; # Initialize loop counter
- # Copy files in list_copy to dir_dest;
- for key in "${!list_copy[@]}"; do
- value="${list_copy[$key]}";
- ## Get basename of path
- file_basename="$(basename "$key")";
-
- ## Get 16-character b2sum fingerprint (for different files that share basename)
- fingerprint="$(b2sum -l64 "$key" | cut -d' ' -f1)";
-
- ## Form output filename
- file_name="$fingerprint".."$file_basename";
- file_name="${file_name:0:$max_filename_length}"; # Limit filename length (e.g. Windows has max of 255 characters)
-
- ## Form output path
- path_output="$dir_dest"/"$file_name";
-
- ## Copy
- try cp "$key" "$path_output" && yell "NOTICE:Copied ($value seconds): $key ";
- #yell "DEBUG:Copied $file_basename to $dur_dest.";
-
- ## Append log
- path_log_output="$dir_dest"/COPY.log;
- if [[ $n -le 0 ]]; then
- echo "fingerprint","duration","original_path" >> "$path_log_output";
- else
- echo "$fingerprint","$value","$key" >> "$path_log_output";
- fi;
-
- ((n++));
- unset file_basename path_output
- done;
-
- # Report total duration
- yell "NOTICE:Total duration (seconds):$dur";
-
-} # Main program
-
-main "$@";
-
-# Author: Steven Baltakatei Sandoval
-# License: GPLv3+