2 # Desc: Wrapper for mpv that accepts directory or file paths via posargs or stdin lines 
   5 # Depends: GNU Parallel, GNU Bash v5.1.16, mpv v0.34.1, bc v1.07.1 
   6 # Ref/Attrib: [1] Tange, Ole. GNU Parallel with Bash Array. 2019-03-24. https://unix.stackexchange.com/a/508365/411854 
   7 # Example: find $HOME/Music -type d | bkmpv2 
   8 # Example: bkmpv2 $HOME/Music/ 
   9 # Example: find $HOME -type f -name "*.mp3" | bkmpv2 
  10 # Note: Does not follow symlinks 
  13 firegex
=".+\(aac\|aif\|aiff\|flac\|m4a\|mp3\|mp4\|ogg\|opus\|wav\|webm\)$"; # POSIX regex for find. Update according to `find . -type f | grep -Eo "\.([[:alnum:]])+$" | sort -u` 
  14 file_regex
=".+(aac|aif|aiff|flac|m4a|mp3|mp4|ogg|opus|wav|webm)$"; # extended regex for Bash. 
  15 fsize
="10k"; # default: minimum "10k" 
  16 fdepth_posarg
="10"; # find depth for positional arguments 
  17 fdepth_stdin
="1";  # find depth for stdin 
  18 fc_falloff
="1000"; # characteristic file count falloff in the output 
  19 fc_redbase
="10"; # logarithm base to reduce output file count 
  21 export firegex fsize 
; # export for parallel 
  23 #===Declare local functions=== 
  24 yell
() { echo "$0: $*" >&2; } # print script path and all args to stderr 
  25 die
() { yell 
"$*"; exit 111; } # same as yell() but non-zero exit status 
  26 must
() { "$@" || die 
"cannot $*"; } # runs args as command, reports args if command fails 
  28     # Desc: If arg is a command, save result in assoc array 'appRollCall' 
  29     # Usage: checkapp arg1 arg2 arg3 ... 
  31     # Input: global assoc. array 'appRollCall' 
  32     # Output: adds/updates key(value) to global assoc array 'appRollCall' 
  38         if command -v "$arg" 1>/dev
/null 
2>&1; then # Check if arg is a valid command 
  39             appRollCall
[$arg]="true"; 
  40             if ! [ "$returnState" = "false" ]; then returnState
="true"; fi; 
  42             appRollCall
[$arg]="false"; returnState
="false"; 
  46     #===Determine function return code=== 
  47     if [ "$returnState" = "true" ]; then 
  52 } # Check that app exists 
  54     # Desc: If arg is a file path, save result in assoc array 'fileRollCall' 
  55     # Usage: checkfile arg1 arg2 arg3 ... 
  57     # Input: global assoc. array 'fileRollCall' 
  58     # Output: adds/updates key(value) to global assoc array 'fileRollCall'; 
  59     # Output: returns 0 if app found, 1 otherwise 
  65         if [ -f "$arg" ]; then 
  66             fileRollCall
["$arg"]="true"; 
  67             if ! [ "$returnState" = "false" ]; then returnState
="true"; fi; 
  69             fileRollCall
["$arg"]="false"; returnState
="false"; 
  73     #===Determine function return code=== 
  74     if [ "$returnState" = "true" ]; then 
  79 } # Check that file exists 
  81     # Desc: If arg is a dir path, save result in assoc array 'dirRollCall' 
  82     # Usage: checkdir arg1 arg2 arg3 ... 
  84     # Input: global assoc. array 'dirRollCall' 
  85     # Output: adds/updates key(value) to global assoc array 'dirRollCall'; 
  86     # Output: returns 0 if app found, 1 otherwise 
  92         if [ -d "$arg" ]; then 
  93             dirRollCall
["$arg"]="true"; 
  94             if ! [ "$returnState" = "false" ]; then returnState
="true"; fi 
  96             dirRollCall
["$arg"]="false"; returnState
="false"; 
 100     #===Determine function return code=== 
 101     if [ "$returnState" = "true" ]; then 
 106 } # Check that dir exists 
 108     # Desc: Displays missing apps, files, and dirs 
 109     # Usage: displayMissing 
 111     # Input: associative arrays: appRollCall, fileRollCall, dirRollCall 
 112     # Output: stderr: messages indicating missing apps, file, or dirs 
 113     # Depends: bash 5, checkAppFileDir() 
 114     local missingApps value appMissing missingFiles fileMissing
 
 115     local missingDirs dirMissing
 
 117     #==BEGIN Display errors== 
 118     #===BEGIN Display Missing Apps=== 
 119     missingApps
="Missing apps  :"; 
 120     #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done 
 121     for key 
in "${!appRollCall[@]}"; do 
 122         value
="${appRollCall[$key]}"; 
 123         if [ "$value" = "false" ]; then 
 124             #echo "DEBUG:Missing apps: $key => $value"; 
 125             missingApps
="$missingApps""$key "; 
 129     if [ "$appMissing" = "true" ]; then  # Only indicate if an app is missing. 
 130         echo "$missingApps" 1>&2; 
 133     #===END Display Missing Apps=== 
 135     #===BEGIN Display Missing Files=== 
 136     missingFiles
="Missing files:"; 
 137     #for key in "${!fileRollCall[@]}"; do echo "DEBUG:$key => ${fileRollCall[$key]}"; done 
 138     for key 
in "${!fileRollCall[@]}"; do 
 139         value
="${fileRollCall[$key]}"; 
 140         if [ "$value" = "false" ]; then 
 141             #echo "DEBUG:Missing files: $key => $value"; 
 142             missingFiles
="$missingFiles""$key "; 
 146     if [ "$fileMissing" = "true" ]; then  # Only indicate if an app is missing. 
 147         echo "$missingFiles" 1>&2; 
 150     #===END Display Missing Files=== 
 152     #===BEGIN Display Missing Directories=== 
 153     missingDirs
="Missing dirs:"; 
 154     #for key in "${!dirRollCall[@]}"; do echo "DEBUG:$key => ${dirRollCall[$key]}"; done 
 155     for key 
in "${!dirRollCall[@]}"; do 
 156         value
="${dirRollCall[$key]}"; 
 157         if [ "$value" = "false" ]; then 
 158             #echo "DEBUG:Missing dirs: $key => $value"; 
 159             missingDirs
="$missingDirs""$key "; 
 163     if [ "$dirMissing" = "true" ]; then  # Only indicate if an dir is missing. 
 164         echo "$missingDirs" 1>&2; 
 167     #===END Display Missing Directories=== 
 169     #==END Display errors== 
 170 } # Display missing apps, files, dirs 
 172     if ! checkapp mpv parallel bkshuf 
bc b2sum
; then 
 174         die 
"FATAL:Missing apps."; 
 177 }; # check dependencies 
 179     # Desc: Checks if arg is integer 
 180     # Usage: checkInt arg 
 181     # Input: arg: integer 
 182     # Output: - return code 0 (if arg is integer) 
 183     #         - return code 1 (if arg is not integer) 
 184     # Example: if ! checkInt $arg; then echo "not int"; fi; 
 189     if [[ $# -ne 1 ]]; then 
 190         die 
"ERROR:Invalid number of arguments:$#"; 
 193     RETEST1
='^[0-9]+$'; # Regular Expression to test 
 194     if [[ ! $1 =~ 
$RETEST1 ]] ; then 
 200     #===Determine function return code=== 
 201     if [ "$returnState" = "true" ]; then 
 206 } # Checks if arg is integer 
 208     # Desc: Consumes stdin; outputs as stdout lines 
 209     # Input: stdin (consumes) 
 210     # Output: stdout (newline delimited) 
 211     # Example: printf "foo\nbar\n" | read_stdin 
 212     # Depends: GNU bash (version 5.1.16) 
 214     local input_stdin output
; 
 217     if [[ -p /dev
/stdin 
]]; then 
 218         input_stdin
="$(cat -)"; 
 221     # Store as output array elements 
 223     if [[ -n $input_stdin ]]; then 
 224         while read -r line
; do 
 226         done < <(printf "%s\n" "$input_stdin"); 
 230     printf "%s\n" "${output[@]}"; 
 231 }; # read stdin to stdout lines 
 233     # Desc: Reads arguments; outputs as stdout lines 
 235     # Output: stdout (newline delimited) 
 236     # Example: read_psarg "$@" 
 237     # Depends: GNU bash (version 5.1.16) 
 239     local input_psarg output
; 
 242     if [[ $# -gt 0 ]]; then 
 246     # Store as output array elements 
 247     ## Read in positional arguments 
 248     if [[ -n $input_psarg ]]; then 
 255     printf "%s\n" "${output[@]}"; 
 256 }; # read positional argument to stdout lines 
 258     # Desc: print file list to stdout via `find` using script parameters 
 259     # Input: arg1: path to dir 
 261     #        var: pattern_find_iregex 
 263     if [[ ! -d "$1" ]]; then return 1; fi; 
 264     must 
find "$1" -maxdepth "$fdepth" -type f 
-iregex "$firegex" -size +"$fsize"; 
 265 }; # print file list to stdout from dir with script parameters 
 267     # Desc: Applies $file_regex to files specified by path 
 268     # Input:  var:   file_regex 
 270     # Output: array: files_stdin 
 272     declare -a filtered_files_stdin
; 
 274     for file in "${files_stdin[@]}"; do 
 275         if [[ "$file" =~ 
$file_regex ]]; then 
 276             filtered_files_stdin
+=("$file"); 
 279     files_stdin
=("${filtered_files_stdin[@]}"); 
 280 }; # apply $firegex to files_stdin array 
 282     # Input: var: firegex     find iregex file name pattern 
 283     #        var: fsize       find minimum file siz 
 284     #        var: fc_falloff  characteristic file count falloff in the output 
 285     #        var: fc_redbase  logarithm base to reduce output file count 
 288     declare -a files_stdin dirs_stdin dirs_psarg
; 
 289     declare -a paths_files
; 
 290     declare list_paths_files
; 
 294     yell 
"STATUS:$SECONDS:Started."; 
 295     #Populate dirs_stdin and dirs_psarg arrays 
 296     ## Read stdin as lines 
 297     re_dotfile
="^\."; # first char is a dot 
 298     while read -r line
; do 
 299         line
="$(readlink -e "$line")"; 
 300         line_bn
="$(basename "$line")"; 
 301         # Check if dir and not dotfile 
 302         if [[ -d "$line" ]] && [[ ! "$line_bn" =~ 
$re_dotfile ]]; then 
 303             dirs_stdin
+=("$line"); 
 304             yell 
"DEBUG:Is a dir:${line}"; # debug 
 309         if [[ -f "$line" ]]; then 
 310             files_stdin
+=("$line"); 
 311             yell 
"DEBUG:Is a file:${line}"; # debug 
 316         yell 
"WARNING:Not a valid dir or file:$line"; 
 317     done < <( read_stdin
; read_psarg 
"$@"; ); 
 318     yell 
"STATUS:$SECONDS:Read stdin and psargs."; 
 320     # Apply the $file_regex to $files_stdin array 
 321     declare -p files_stdin
; 
 323     declare -p files_stdin
; 
 325     # Catch all arrays empty 
 326     if [[ "${#dirs_stdin[@]}" -le 0 ]] && \
 
 327            [[ "${#dirs_psarg[@]}" -le 0 ]] && \
 
 328            [[ "${#files_stdin[@]}" -le 0 ]]; then 
 329         die 
"FATAL:No valid directories or files provided."; 
 333     ## Add stdin argument input 
 334     if [[ "${#files_stdin[@]}" -gt 0 ]]; then 
 335         paths_files
+=("${files_stdin[@]}"); 
 338     ## Call find_filelist() in parallel for positional argument input 
 339     if [[ "${#dirs_psarg[@]}" -gt 0 ]]; then 
 340         fdepth
="$fdepth_posarg"; export fdepth
; # for dirs from positional arguments 
 341         paths_files
+=("$( parallel find_flist {} "$fdepth" "$firegex" "$fsize" ::: "${dirs_psarg[@]}" )"); # See [1] 
 343     ## Call find_filelist() in parallel for stdin input 
 344     if [[ "${#dirs_stdin[@]}" -gt 0 ]]; then 
 345         fdepth
="$fdepth_stdin"; export fdepth
; # 1 for dirs from stdin 
 346         paths_files
+=("$( parallel find_flist {} "$fdepth" "$firegex" "$fsize" ::: "${dirs_stdin[@]}" )"); # See [1] 
 349     # Convert paths_files array into file list 
 350     for i 
in "${!paths_files[@]}"; do 
 351         list_paths_files
="$(printf "%s
\n%s
" "${paths_files[$i]}" "$list_paths_files")"; 
 354     fc="$(wc -l < <(echo -n "$list_paths_files"))"; 
 355     echo "STATUS:$SECONDS:file count:fc:$fc" 
 356     yell 
"STATUS:$SECONDS:Generated file list."; 
 358     # Sort, remove duplicate paths 
 359     list_paths_files
="$(echo "$list_paths_files" | sort -u | tr -s '\n')"; 
 360     yell 
"STATUS:$SECONDS:Sorted and deduplicated list."; 
 362     # Remove paths with dotfiles 
 363     list_paths_files
="$(echo "$list_paths_files" | grep -viE "/\.
" )"; 
 364     yell 
"STATUS:$SECONDS:Removed dotfiles."; 
 367     list_paths_files_tmp
="/dev/shm/$(date +%Y%m%dT%H%M%S.%N%z)"..mpv_paths.txt
; 
 368     ## Reduce output file count if greater than falloff 
 369     if [[ $fc -gt $fc_falloff ]]; then 
 370             bc_exp
="$fc_falloff * (1 + l( $fc / $fc_falloff )/l($fc_redbase))"; 
 371             fc_out
="$( echo "$bc_exp" | bc -l )"; 
 375     ## Reduce output file count by fixed fraction (bkshuf optimization) 
 376     fc_out
="$(echo "$fc_out * 0.75" | bc -l)"; 
 377     ## Select subset via bkshuf 
 378     ### Specify bkshuf-specific environment variables 
 379     export BKSHUF_PARAM_LINC
=1000000; # for these numbers of lines of input.. 
 380     export BKSHUF_PARAM_GSIZE
=25;     # target this group size 
 381     ### Round file count down 
 382     fc_out
="$(printf "%.0f
" "$fc_out")"; # round float down to int 
 383     ### Get neighbor-preserving shuffled subset (size $fc_out) 
 384     yell 
"STATUS:$SECONDS:Selecting $fc_out of $fc files via bkshuf..."; 
 386         echo -n "$list_paths_files" | \
 
 387         bkshuf 
"$fc_out" > "$list_paths_files_tmp"; 
 388     yell 
"STATUS:$SECONDS:Wrote playlist."; 
 391     yell 
"STATUS:$SECONDS:Built file list in $SECONDS seconds."; 
 393     # Run mpv with filelist 
 396     cmd_args
+=("--audio-display=no"); # disable video for audio files 
 397     cmd_args
+=("--vid=no"); # donʼt show video track 
 398     cmd_args
+=("--image-display-duration=0"); # don't show album art 
 399     cmd_args
+=("--playlist=$list_paths_files_tmp"); # read playlist 
 400     declare -p cmd_args
; # debug 
 402     must 
"${cmd_args[@]}"; 
 403     must 
rm "$list_paths_files_tmp"; 
 405 export -f yell die must read_stdin read_psarg find_flist
; 
 409 # Author: Steven Baltakatei Sandoval