Commit | Line | Data |
---|---|---|
75e92939 | 1 | #!/usr/bin/env bash |
75baf701 | 2 | # Desc: Wrapper for mpv that accepts directory or file paths via posargs or stdin lines |
75e92939 | 3 | # Usage: bkmpv2 [DIR] |
e41f7d05 | 4 | # Version: 0.2.0 |
75e92939 SBS |
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/ | |
75baf701 | 9 | # Example: find $HOME -type f -name "*.mp3" | bkmpv2 |
75e92939 SBS |
10 | # Note: Does not follow symlinks |
11 | ||
12 | # Find settings | |
e41f7d05 SBS |
13 | firegex=".+\(aac\|aif\|aiff\|flac\|m4a\|mp3\|mp4\|ogg\|opus\|wav\)$"; # 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)$"; # extended regex for Bash. | |
75e92939 SBS |
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 | |
20 | ||
21 | export firegex fsize ; # export for parallel | |
22 | ||
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 | |
27 | checkapp() { | |
28 | # Desc: If arg is a command, save result in assoc array 'appRollCall' | |
29 | # Usage: checkapp arg1 arg2 arg3 ... | |
30 | # Version: 0.1.1 | |
31 | # Input: global assoc. array 'appRollCall' | |
32 | # Output: adds/updates key(value) to global assoc array 'appRollCall' | |
33 | # Depends: bash 5.0.3 | |
75baf701 | 34 | local returnState |
75e92939 SBS |
35 | |
36 | #===Process Args=== | |
37 | for arg in "$@"; do | |
75baf701 SBS |
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; | |
41 | else | |
42 | appRollCall[$arg]="false"; returnState="false"; | |
43 | fi; | |
75e92939 SBS |
44 | done; |
45 | ||
46 | #===Determine function return code=== | |
47 | if [ "$returnState" = "true" ]; then | |
75baf701 | 48 | return 0; |
75e92939 | 49 | else |
75baf701 | 50 | return 1; |
75e92939 SBS |
51 | fi; |
52 | } # Check that app exists | |
53 | checkfile() { | |
54 | # Desc: If arg is a file path, save result in assoc array 'fileRollCall' | |
55 | # Usage: checkfile arg1 arg2 arg3 ... | |
56 | # Version: 0.1.1 | |
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 | |
60 | # Depends: bash 5.0.3 | |
61 | local returnState | |
62 | ||
63 | #===Process Args=== | |
64 | for arg in "$@"; do | |
75baf701 SBS |
65 | if [ -f "$arg" ]; then |
66 | fileRollCall["$arg"]="true"; | |
67 | if ! [ "$returnState" = "false" ]; then returnState="true"; fi; | |
68 | else | |
69 | fileRollCall["$arg"]="false"; returnState="false"; | |
70 | fi; | |
75e92939 | 71 | done; |
75baf701 | 72 | |
75e92939 SBS |
73 | #===Determine function return code=== |
74 | if [ "$returnState" = "true" ]; then | |
75baf701 | 75 | return 0; |
75e92939 | 76 | else |
75baf701 | 77 | return 1; |
75e92939 SBS |
78 | fi; |
79 | } # Check that file exists | |
80 | checkdir() { | |
81 | # Desc: If arg is a dir path, save result in assoc array 'dirRollCall' | |
82 | # Usage: checkdir arg1 arg2 arg3 ... | |
83 | # Version 0.1.1 | |
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 | |
87 | # Depends: Bash 5.0.3 | |
88 | local returnState | |
89 | ||
90 | #===Process Args=== | |
91 | for arg in "$@"; do | |
75baf701 SBS |
92 | if [ -d "$arg" ]; then |
93 | dirRollCall["$arg"]="true"; | |
94 | if ! [ "$returnState" = "false" ]; then returnState="true"; fi | |
95 | else | |
96 | dirRollCall["$arg"]="false"; returnState="false"; | |
97 | fi | |
75e92939 | 98 | done |
75baf701 | 99 | |
75e92939 SBS |
100 | #===Determine function return code=== |
101 | if [ "$returnState" = "true" ]; then | |
75baf701 | 102 | return 0; |
75e92939 | 103 | else |
75baf701 | 104 | return 1; |
75e92939 SBS |
105 | fi |
106 | } # Check that dir exists | |
107 | displayMissing() { | |
108 | # Desc: Displays missing apps, files, and dirs | |
109 | # Usage: displayMissing | |
110 | # Version 0.1.1 | |
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 | |
75baf701 | 116 | |
75e92939 SBS |
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 | |
75baf701 SBS |
122 | value="${appRollCall[$key]}"; |
123 | if [ "$value" = "false" ]; then | |
124 | #echo "DEBUG:Missing apps: $key => $value"; | |
125 | missingApps="$missingApps""$key "; | |
126 | appMissing="true"; | |
127 | fi; | |
75e92939 SBS |
128 | done; |
129 | if [ "$appMissing" = "true" ]; then # Only indicate if an app is missing. | |
75baf701 | 130 | echo "$missingApps" 1>&2; |
75e92939 SBS |
131 | fi; |
132 | unset value; | |
133 | #===END Display Missing Apps=== | |
134 | ||
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 | |
75baf701 SBS |
139 | value="${fileRollCall[$key]}"; |
140 | if [ "$value" = "false" ]; then | |
141 | #echo "DEBUG:Missing files: $key => $value"; | |
142 | missingFiles="$missingFiles""$key "; | |
143 | fileMissing="true"; | |
144 | fi; | |
75e92939 SBS |
145 | done; |
146 | if [ "$fileMissing" = "true" ]; then # Only indicate if an app is missing. | |
75baf701 | 147 | echo "$missingFiles" 1>&2; |
75e92939 SBS |
148 | fi; |
149 | unset value; | |
150 | #===END Display Missing Files=== | |
151 | ||
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 | |
75baf701 SBS |
156 | value="${dirRollCall[$key]}"; |
157 | if [ "$value" = "false" ]; then | |
158 | #echo "DEBUG:Missing dirs: $key => $value"; | |
159 | missingDirs="$missingDirs""$key "; | |
160 | dirMissing="true"; | |
161 | fi; | |
75e92939 SBS |
162 | done; |
163 | if [ "$dirMissing" = "true" ]; then # Only indicate if an dir is missing. | |
75baf701 | 164 | echo "$missingDirs" 1>&2; |
75e92939 SBS |
165 | fi; |
166 | unset value; | |
167 | #===END Display Missing Directories=== | |
168 | ||
169 | #==END Display errors== | |
170 | } # Display missing apps, files, dirs | |
171 | check_depends() { | |
172 | if ! checkapp mpv parallel bkshuf bc b2sum; then | |
173 | displayMissing; | |
174 | die "FATAL:Missing apps."; | |
175 | fi; | |
176 | return 1; | |
177 | }; # check dependencies | |
178 | checkInt() { | |
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; | |
185 | # Version: 0.0.1 | |
186 | local returnState | |
187 | ||
188 | #===Process Arg=== | |
189 | if [[ $# -ne 1 ]]; then | |
75baf701 | 190 | die "ERROR:Invalid number of arguments:$#"; |
75e92939 | 191 | fi; |
75baf701 | 192 | |
75e92939 SBS |
193 | RETEST1='^[0-9]+$'; # Regular Expression to test |
194 | if [[ ! $1 =~ $RETEST1 ]] ; then | |
75baf701 | 195 | returnState="false"; |
75e92939 | 196 | else |
75baf701 | 197 | returnState="true"; |
75e92939 SBS |
198 | fi; |
199 | ||
200 | #===Determine function return code=== | |
201 | if [ "$returnState" = "true" ]; then | |
75baf701 | 202 | return 0; |
75e92939 | 203 | else |
75baf701 | 204 | return 1; |
75e92939 SBS |
205 | fi; |
206 | } # Checks if arg is integer | |
207 | read_stdin() { | |
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) | |
213 | # Version: 0.0.1 | |
214 | local input_stdin output; | |
215 | ||
216 | # Store stdin | |
217 | if [[ -p /dev/stdin ]]; then | |
218 | input_stdin="$(cat -)"; | |
75baf701 SBS |
219 | fi; |
220 | ||
75e92939 SBS |
221 | # Store as output array elements |
222 | ## Read in stdin | |
223 | if [[ -n $input_stdin ]]; then | |
224 | while read -r line; do | |
225 | output+=("$line"); | |
226 | done < <(printf "%s\n" "$input_stdin"); | |
227 | fi; | |
228 | ||
229 | # Print to stdout | |
230 | printf "%s\n" "${output[@]}"; | |
231 | }; # read stdin to stdout lines | |
232 | read_psarg() { | |
233 | # Desc: Reads arguments; outputs as stdout lines | |
234 | # Input: args | |
235 | # Output: stdout (newline delimited) | |
236 | # Example: read_psarg "$@" | |
237 | # Depends: GNU bash (version 5.1.16) | |
238 | # Version: 0.0.1 | |
239 | local input_psarg output; | |
75baf701 | 240 | |
75e92939 SBS |
241 | # Store arguments |
242 | if [[ $# -gt 0 ]]; then | |
243 | input_psarg="$*"; | |
244 | fi; | |
75baf701 | 245 | |
75e92939 SBS |
246 | # Store as output array elements |
247 | ## Read in positional arguments | |
248 | if [[ -n $input_psarg ]]; then | |
249 | for arg in "$@"; do | |
250 | output+=("$arg"); | |
251 | done; | |
252 | fi; | |
253 | ||
254 | # Print to stdout | |
255 | printf "%s\n" "${output[@]}"; | |
256 | }; # read positional argument to stdout lines | |
257 | find_flist() { | |
258 | # Desc: print file list to stdout via `find` using script parameters | |
259 | # Input: arg1: path to dir | |
260 | # var: find_depth | |
261 | # var: pattern_find_iregex | |
262 | # var: find_size | |
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 | |
e41f7d05 SBS |
266 | check_files() { |
267 | # Desc: Applies $file_regex to files specified by path | |
268 | # Input: var: file_regex | |
269 | # array: files_stdin | |
270 | # Output: array: files_stdin | |
271 | local file; | |
272 | declare -a filtered_files_stdin; | |
273 | ||
274 | for file in "${files_stdin[@]}"; do | |
275 | if [[ "$file" =~ $file_regex ]]; then | |
276 | filtered_files_stdin+=("$file"); | |
277 | fi; | |
278 | done; | |
279 | files_stdin=("${filtered_files_stdin[@]}"); | |
280 | }; # apply $firegex to files_stdin array | |
75e92939 SBS |
281 | main() { |
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 | |
286 | ||
287 | local re_dotfile; | |
75baf701 | 288 | declare -a files_stdin dirs_stdin dirs_psarg; |
75e92939 SBS |
289 | declare -a paths_files; |
290 | declare list_paths_files; | |
291 | declare -a cmd_args; | |
292 | check_depends; | |
293 | ||
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")"; | |
75baf701 SBS |
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"); | |
75e92939 SBS |
304 | continue; |
305 | fi; | |
75baf701 SBS |
306 | |
307 | # Check if file | |
308 | if [[ -f "$line" ]]; then | |
309 | files_stdin+=("$line"); | |
75e92939 SBS |
310 | continue; |
311 | fi; | |
75baf701 SBS |
312 | |
313 | # Throw warning | |
314 | yell "WARNING:Not a valid dir or file:$line"; | |
315 | done < <( read_stdin; read_psarg "$@"; ); | |
316 | yell "STATUS:$SECONDS:Read stdin and psargs."; | |
317 | ||
e41f7d05 SBS |
318 | # Apply the $file_regex to $files_stdin array |
319 | check_files; | |
320 | ||
75e92939 | 321 | # Catch all arrays empty |
75baf701 SBS |
322 | if [[ "${#dirs_stdin[@]}" -le 0 ]] && \ |
323 | [[ "${#dirs_psarg[@]}" -le 0 ]] && \ | |
324 | [[ "${#files_stdin[@]}" -le 0 ]]; then | |
325 | die "FATAL:No valid directories or files provided."; | |
75e92939 SBS |
326 | fi; |
327 | ||
328 | # Generate file list | |
75baf701 SBS |
329 | ## Add stdin argument input |
330 | if [[ "${#files_stdin[@]}" -gt 0 ]]; then | |
331 | paths_files+=("${files_stdin[@]}"); | |
332 | fi; | |
333 | ||
75e92939 SBS |
334 | ## Call find_filelist() in parallel for positional argument input |
335 | if [[ "${#dirs_psarg[@]}" -gt 0 ]]; then | |
336 | fdepth="$fdepth_posarg"; export fdepth; # for dirs from positional arguments | |
337 | paths_files+=("$( parallel find_flist {} "$fdepth" "$firegex" "$fsize" ::: "${dirs_psarg[@]}" )"); # See [1] | |
338 | fi; | |
339 | ## Call find_filelist() in parallel for stdin input | |
340 | if [[ "${#dirs_stdin[@]}" -gt 0 ]]; then | |
341 | fdepth="$fdepth_stdin"; export fdepth; # 1 for dirs from stdin | |
342 | paths_files+=("$( parallel find_flist {} "$fdepth" "$firegex" "$fsize" ::: "${dirs_stdin[@]}" )"); # See [1] | |
343 | fi; | |
344 | ||
345 | # Convert paths_files array into file list | |
346 | for i in "${!paths_files[@]}"; do | |
347 | list_paths_files="$(printf "%s\n%s" "${paths_files[$i]}" "$list_paths_files")"; | |
348 | done; | |
349 | # Get stats | |
350 | fc="$(wc -l < <(echo -n "$list_paths_files"))"; | |
351 | echo "STATUS:$SECONDS:file count:fc:$fc" | |
352 | yell "STATUS:$SECONDS:Generated file list."; | |
353 | ||
354 | # Sort, remove duplicate paths | |
355 | list_paths_files="$(echo "$list_paths_files" | sort -u | tr -s '\n')"; | |
356 | yell "STATUS:$SECONDS:Sorted and deduplicated list."; | |
357 | ||
358 | # Remove paths with dotfiles | |
359 | list_paths_files="$(echo "$list_paths_files" | grep -viE "/\." )"; | |
360 | yell "STATUS:$SECONDS:Removed dotfiles."; | |
361 | ||
362 | # Write playlist | |
363 | list_paths_files_tmp="/dev/shm/$(date +%Y%m%dT%H%M%S.%N%z)"..mpv_paths.txt; | |
364 | ## Reduce output file count if greater than falloff | |
365 | if [[ $fc -gt $fc_falloff ]]; then | |
366 | bc_exp="$fc_falloff * (1 + l( $fc / $fc_falloff )/l($fc_redbase))"; | |
367 | fc_out="$( echo "$bc_exp" | bc -l )"; | |
368 | else | |
75baf701 | 369 | fc_out="$fc"; |
75e92939 SBS |
370 | fi; |
371 | ## Reduce output file count by fixed fraction (bkshuf optimization) | |
372 | fc_out="$(echo "$fc_out * 0.75" | bc -l)"; | |
373 | ## Select subset via bkshuf | |
374 | ### Specify bkshuf-specific environment variables | |
375 | export BKSHUF_PARAM_LINC=1000000; # for these numbers of lines of input.. | |
376 | export BKSHUF_PARAM_GSIZE=25; # target this group size | |
377 | ### Round file count down | |
378 | fc_out="$(printf "%.0f" "$fc_out")"; # round float down to int | |
379 | ### Get neighbor-preserving shuffled subset (size $fc_out) | |
75baf701 | 380 | yell "STATUS:$SECONDS:Selecting $fc_out of $fc files via bkshuf..."; |
75e92939 SBS |
381 | must \ |
382 | echo -n "$list_paths_files" | \ | |
383 | bkshuf "$fc_out" > "$list_paths_files_tmp"; | |
384 | yell "STATUS:$SECONDS:Wrote playlist."; | |
385 | ||
386 | # Print stats | |
387 | yell "STATUS:$SECONDS:Built file list in $SECONDS seconds."; | |
388 | ||
389 | # Run mpv with filelist | |
390 | ## Form command | |
391 | cmd_args+=("mpv"); | |
392 | cmd_args+=("--audio-display=no"); # disable video for audio files | |
393 | cmd_args+=("--vid=no"); # donʼt show video track | |
394 | cmd_args+=("--image-display-duration=0"); # don't show album art | |
395 | cmd_args+=("--af=scaletempo=stride=15:overlap=1:search=15"); # improve scrubbing | |
396 | cmd_args+=("--playlist=$list_paths_files_tmp"); # read playlist | |
397 | declare -p cmd_args; # debug | |
398 | ## Execute command | |
399 | must "${cmd_args[@]}"; | |
400 | must rm "$list_paths_files_tmp"; | |
401 | }; | |
402 | export -f yell die must read_stdin read_psarg find_flist; | |
403 | ||
404 | main "$@"; | |
405 | ||
406 | # Author: Steven Baltakatei Sandoval | |
407 | # License: GPLv3+ |