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